diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 14908f87bc0..a9c453eeed9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,7 +1,13 @@ name: Publish to PyPI on: + release: + types: [published] workflow_dispatch: + inputs: + tag: + description: "Release tag (e.g. v1.2.3 or reflex-lucide-v0.1.0)" + required: true jobs: publish: @@ -14,14 +20,38 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v7 + - name: Parse release tag + id: parse + run: | + TAG="${{ github.event.release.tag_name || inputs.tag }}" + # Tag format: v1.2.3 for reflex, reflex-lucide-v0.1.0 for sub-packages + if [[ "$TAG" =~ ^v([0-9].*)$ ]]; then + echo "package=reflex" >> "$GITHUB_OUTPUT" + echo "build_dir=." >> "$GITHUB_OUTPUT" + elif [[ "$TAG" =~ ^(.+)-v([0-9].*)$ ]]; then + PACKAGE="${BASH_REMATCH[1]}" + if [ ! -d "packages/$PACKAGE" ]; then + echo "Error: packages/$PACKAGE does not exist" + exit 1 + fi + echo "package=$PACKAGE" >> "$GITHUB_OUTPUT" + echo "build_dir=packages/$PACKAGE" >> "$GITHUB_OUTPUT" + else + echo "Error: Tag '$TAG' does not match expected format (v* or -v*)" + exit 1 + fi + - name: Build - run: uv build + run: uv build --directory "${{ steps.parse.outputs.build_dir }}" - name: Verify .pyi files in wheel + if: steps.parse.outputs.package == 'reflex' run: | if unzip -l dist/*.whl | grep '\.pyi$'; then echo "✓ .pyi files found in distribution" @@ -31,4 +61,4 @@ jobs: fi - name: Publish - run: uv publish + run: uv publish --directory "${{ steps.parse.outputs.build_dir }}" diff --git a/docs/DEBUGGING.md b/DEBUGGING.md similarity index 100% rename from docs/DEBUGGING.md rename to DEBUGGING.md diff --git a/docs/__init__.py b/docs/__init__.py new file mode 100644 index 00000000000..10ffd1f365c --- /dev/null +++ b/docs/__init__.py @@ -0,0 +1 @@ +"""Reflex documentation.""" diff --git a/docs/advanced_onboarding/code_structure.md b/docs/advanced_onboarding/code_structure.md new file mode 100644 index 00000000000..70c53648aa2 --- /dev/null +++ b/docs/advanced_onboarding/code_structure.md @@ -0,0 +1,365 @@ +```python exec +from pcweb.pages.docs import custom_components +``` + +# Project Structure (Advanced) + +## App Module + +Reflex imports the main app module based on the `app_name` from the config, which **must define a module-level global named `app` as an instance of `rx.App`**. + +The main app module is responsible for importing all other modules that make up the app and defining `app = rx.App()`. + +**All other modules containing pages, state, and models MUST be imported by the main app module or package** for Reflex to include them in the compiled output. + +# Breaking the App into Smaller Pieces + +As applications scale, effective organization is crucial. This is achieved by breaking the application down into smaller, manageable modules and organizing them into logical packages that avoid circular dependencies. + +In the following documentation there will be an app with an `app_name` of `example_big_app`. The main module would be `example_big_app/example_big_app.py`. + +In the [Putting it all together](#putting-it-all-together) section there is a visual of the project folder structure to help follow along with the examples below. + +### Pages Package: `example_big_app/pages` + +All complex apps will have multiple pages, so it is recommended to create `example_big_app/pages` as a package. + +1. This package should contain one module per page in the app. +2. If a particular page depends on the state, the substate should be defined in the same module as the page. +3. The page-returning function should be decorated with `rx.page()` to have it added as a route in the app. + +```python +import reflex as rx + +from ..state import AuthState + + +class LoginState(AuthState): + @rx.event + def handle_submit(self, form_data): + self.logged_in = authenticate(form_data["username"], form_data["password"]) + + +def login_field(name: str, **input_props): + return rx.hstack( + rx.text(name.capitalize()), + rx.input(name=name, **input_props), + width="100%", + justify="between", + ) + + +@rx.page(route="/login") +def login(): + return rx.card( + rx.form( + rx.vstack( + login_field("username"), + login_field("password", type="password"), + rx.button("Login"), + width="100%", + justify="center", + ), + on_submit=LoginState.handle_submit, + ), + ) +``` + +### Templating: `example_big_app/template.py` + +Most applications maintain a consistent layout and structure across pages. Defining this common structure in a separate module facilitates easy sharing and reuse when constructing individual pages. + +**Best Practices** + +1. Factor out common frontend UI elements into a function that returns a component. +2. If a function accepts a function that returns a component, it can be used as a decorator as seen below. + +```python +from typing import Callable + +import reflex as rx + +from .components.menu import menu +from .components.navbar import navbar + + +def template(page: Callable[[], rx.Component]) -> rx.Component: + return rx.vstack( + navbar(), + rx.hstack( + menu(), + rx.container(page()), + ), + width="100%", + ) +``` + +The `@template` decorator should appear below the `@rx.page` decorator and above the page-returning function. See the [Posts Page](#a-post-page-example_big_apppagespostspy) code for an example. + +## State Management + +Most pages will use State in some capacity. You should avoid adding vars to a +shared state that will only be used in a single page. Instead, define a new +subclass of `rx.State` and keep it in the same module as the page. + +### Accessing other States + +As of Reflex 0.4.3, any event handler can get access to an instance of any other +substate via the `get_state` API. From a practical perspective, this means that +state can be split up into smaller pieces without requiring a complex +inheritance hierarchy to share access to other states. + +In previous releases, if an app wanted to store settings in `SettingsState` with +a page or component for modifying them, any other state with an event handler +that needed to access those settings would have to inherit from `SettingsState`, +even if the other state was mostly orthogonal. The other state would also now +always have to load the settings, even for event handlers that didn't need to +access them. + +A better strategy is to load the desired state on demand from only the event +handler which needs access to the substate. + +### A Settings Component: `example_big_app/components/settings.py` + +```python +import reflex as rx + + +class SettingsState(rx.State): + refresh_interval: int = 15 + auto_update: bool = True + prefer_plain_text: bool = True + posts_per_page: int = 20 + + +def settings_dialog(): + return rx.dialog(...) +``` + +### A Post Page: `example_big_app/pages/posts.py` + +This page loads the `SettingsState` to determine how many posts to display per page +and how often to refresh. + +```python +import reflex as rx + +from ..models import Post +from ..template import template +from ..components.settings import SettingsState + + +class PostsState(rx.State): + refresh_tick: int + page: int + posts: list[Post] + + @rx.event + async def on_load(self): + settings = await self.get_state(SettingsState) + if settings.auto_update: + self.refresh_tick = settings.refresh_interval * 1000 + else: + self.refresh_tick = 0 + + @rx.event + async def tick(self, _): + settings = await self.get_state(SettingsState) + with rx.session() as session: + q = Post.select().offset(self.page * settings.posts_per_page).limit(settings.posts_per_page) + self.posts = q.all() + + @rx.event + def go_to_previous(self): + if self.page > 0: + self.page = self.page - 1 + + @rx.event + def go_to_next(self): + if self.posts: + self.page = self.page + 1 + + +@rx.page(route="/posts", on_load=PostsState.on_load) +@template +def posts(): + return rx.vstack( + rx.foreach(PostsState.posts, post_view), + rx.hstack( + rx.button("< Prev", on_click=PostsState.go_to_previous), + rx.button("Next >", on_click=PostsState.go_to_next), + justify="between", + ), + rx.moment(interval=PostsState.refresh_tick, on_change=PostsState.tick, display="none"), + width="100%", + ) +``` + +### Common State: `example_big_app/state.py` + +_Common_ states and substates that are shared by multiple pages or components +should be implemented in a separate module to avoid circular imports. This +module should not import other modules in the app. + +## Component Reusability + +The primary mechanism for reusing components in Reflex is to define a function that returns +the component, then simply call it where that functionality is needed. + +Component functions typically should not take any State classes as arguments, but prefer +to import the needed state and access the vars on the class directly. + +### Memoize Functions for Improved Performance + +In a large app, if a component has many subcomponents or is used in a large number of places, it can improve compile and runtime performance to memoize the function with the `@lru_cache` decorator. + +To memoize the `foo` component to avoid re-creating it many times simply add `@lru_cache` to the function definition, and the component will only be created once per unique set of arguments. + +```python +from functools import lru_cache + +import reflex as rx + +class State(rx.State): + v: str = "foo" + + +@lru_cache +def foo(): + return rx.text(State.v) + + +def index(): + return rx.flex( + rx.button("Change", on_click=State.set_v(rx.cond(State.v != "bar", "bar", "foo"))), + *[ + foo() + for _ in range(100) + ], + direction="row", + wrap="wrap", + ) +``` + +### example_big_app/components + +This package contains reusable parts of the app, for example headers, footers, +and menus. If a particular component requires state, the substate may be defined +in the same module for locality. Any substate defined in a component module +should only contain fields and event handlers pertaining to that individual +component. + +### External Components + +Reflex 0.4.3 introduced support for the [`reflex component` CLI commands]({custom_components.overview.path}), which makes it easy +to bundle up common functionality to publish on PyPI as a standalone Python package +that can be installed and used in any Reflex app. + +When wrapping npm components or other self-contained bits of functionality, it can be helpful +to move this complexity outside the app itself for easier maintenance and reuse in other apps. + +## Database Models: `example_big_app/models.py` + +It is recommended to implement all database models in a single file to make it easier to define relationships and understand the entire schema. + +However, if the schema is very large, it might make sense to have a `models` package with individual models defined in their own modules. + +At any rate, defining the models separately allows any page or component to import and use them without circular imports. + +## Top-level Package: `example_big_app/__init__.py` + +This is a great place to import all state, models, and pages that should be part of the app. +Typically, components and helpers do not need to imported, because they will be imported by +pages that use them (or they would be unused). + +```python +from . import state, models +from .pages import index, login, post, product, profile, schedule + +__all__ = [ + "state", + "models", + "index", + "login", + "post", + "product", + "profile", + "schedule", +] +``` + +If any pages are not imported here, they will not be compiled as part of the app. + +## example_big_app/example_big_app.py + +This is the main app module. Since everything else is defined in other modules, this file becomes very simple. + +```python +import reflex as rx + +app = rx.App() +``` + +## File Management + +There are two categories of non-code assets (media, fonts, stylesheets, +documents) typically used in a Reflex app. + +### assets + +The `assets` directory is used for **static** files that should be accessible +relative to the root of the frontend (default port 3000). When an app is deployed in +production mode, changes to the assets directory will NOT be available at runtime! + +When referencing an asset, always use a leading forward slash, so the +asset can be resolved regardless of the page route where it may appear. + +### uploaded_files + +If an app needs to make files available dynamically at runtime, it is +recommended to set the target directory via `REFLEX_UPLOADED_FILES_DIR` +environment variable (default `./uploaded_files`), write files relative to the +path returned by `rx.get_upload_dir()`, and create working links via +`rx.get_upload_url(relative_path)`. + +Uploaded files are served from the backend (default port 8000) via +`/_upload/` + +## Putting it all together + +Based on the previous discussion, the recommended project layout look like this. + +```text +example-big-app/ +├─ assets/ +├─ example_big_app/ +│ ├─ components/ +│ │ ├─ __init__.py +│ │ ├─ auth.py +│ │ ├─ footer.py +│ │ ├─ menu.py +│ │ ├─ navbar.py +│ ├─ pages/ +│ │ ├─ __init__.py +│ │ ├─ index.py +│ │ ├─ login.py +│ │ ├─ posts.py +│ │ ├─ product.py +│ │ ├─ profile.py +│ │ ├─ schedule.py +│ ├─ __init__.py +│ ├─ example_big_app.py +│ ├─ models.py +│ ├─ state.py +│ ├─ template.py +├─ uploaded_files/ +├─ requirements.txt +├─ rxconfig.py +``` + +## Key Takeaways + +- Like any other Python project, **split up the app into modules and packages** to keep the codebase organized and manageable. +- Using smaller modules and packages makes it easier to **reuse components and state** across the app + without introducing circular dependencies. +- Create **individual functions** to encapsulate units of functionality and **reuse them** where needed. diff --git a/docs/advanced_onboarding/configuration.md b/docs/advanced_onboarding/configuration.md new file mode 100644 index 00000000000..bce5179c727 --- /dev/null +++ b/docs/advanced_onboarding/configuration.md @@ -0,0 +1,60 @@ +```python exec +from pcweb.pages.docs import api_reference +``` + +# Configuration + +Reflex apps can be configured using a configuration file, environment variables, and command line arguments. + +## Configuration File + +Running `reflex init` will create an `rxconfig.py` file in your root directory. +You can pass keyword arguments to the `Config` class to configure your app. + +For example: + +```python +# rxconfig.py +import reflex as rx + +config = rx.Config( + app_name="my_app_name", + # Connect to your own database. + db_url="postgresql://user:password@localhost:5432/my_db", + # Change the frontend port. + frontend_port=3001, +) +``` + +See the [config reference]({api_reference.config.path}) for all the parameters available. + +## Environment Variables + +You can override the configuration file by setting environment variables. +For example, to override the `frontend_port` setting, you can set the `FRONTEND_PORT` environment variable. + +```bash +FRONTEND_PORT=3001 reflex run +``` + +## Command Line Arguments + +Finally, you can override the configuration file and environment variables by passing command line arguments to `reflex run`. + +```bash +reflex run --frontend-port 3001 +``` + +See the [CLI reference]({api_reference.cli.path}) for all the arguments available. + +## Customizable App Data Directory + +The `REFLEX_DIR` environment variable can be set, which allows users to set the location where Reflex writes helper tools like Bun and NodeJS. + +By default we use Platform specific directories: + +On windows, `C:/Users//AppData/Local/reflex` is used. + +On macOS, `~/Library/Application Support/reflex` is used. + +On linux, `~/.local/share/reflex` is used. diff --git a/docs/advanced_onboarding/how-reflex-works.md b/docs/advanced_onboarding/how-reflex-works.md new file mode 100644 index 00000000000..47b42afab50 --- /dev/null +++ b/docs/advanced_onboarding/how-reflex-works.md @@ -0,0 +1,243 @@ +```python exec +from pcweb import constants +from pcweb.constants import REFLEX_ASSETS_CDN +from pcweb.pages.docs import wrapping_react, custom_components, styling, events +from pcweb.pages.docs.custom_components import custom_components as cc +``` + +# How Reflex Works + +We'll use the following basic app that displays Github profile images as an example to explain the different parts of the architecture. + +```python demo exec +import requests +import reflex as rx + +class GithubState(rx.State): + url: str = "https://github.com/reflex-dev" + profile_image: str = "https://avatars.githubusercontent.com/u/104714959" + + @rx.event + def set_profile(self, username: str): + if username == "": + return + try: + github_data = requests.get(f"https://api.github.com/users/{username}").json() + except: + return + self.url = github_data["url"] + self.profile_image = github_data["avatar_url"] + +def index(): + return rx.hstack( + rx.link( + rx.avatar(src=GithubState.profile_image), + href=GithubState.url, + ), + rx.input( + placeholder="Your Github username", + on_blur=GithubState.set_profile, + ), + ) +``` + +## The Reflex Architecture + +Full-stack web apps are made up of a frontend and a backend. The frontend is the user interface, and is served as a web page that runs on the user's browser. The backend handles the logic and state management (such as databases and APIs), and is run on a server. + +In traditional web development, these are usually two separate apps, and are often written in different frameworks or languages. For example, you may combine a Flask backend with a React frontend. With this approach, you have to maintain two separate apps and end up writing a lot of boilerplate code to connect the frontend and backend. + +We wanted to simplify this process in Reflex by defining both the frontend and backend in a single codebase, while using Python for everything. Developers should only worry about their app's logic and not about the low-level implementation details. + +### TLDR + +Under the hood, Reflex apps compile down to a [React](https://react.dev) frontend app and a [FastAPI](https://github.com/tiangolo/fastapi) backend app. Only the UI is compiled to Javascript; all the app logic and state management stays in Python and is run on the server. Reflex uses [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) to send events from the frontend to the backend, and to send state updates from the backend to the frontend. + +The diagram below provides a detailed overview of how a Reflex app works. We'll go through each part in more detail in the following sections. + +```python exec +from reflex_image_zoom import image_zoom +``` + +```python eval +image_zoom(rx.image(src=f"{REFLEX_ASSETS_CDN}other/architecture.webp")) +``` + +```python eval +rx.box(height="1em") +``` + +## Frontend + +We wanted Reflex apps to look and feel like a traditional web app to the end user, while still being easy to build and maintain for the developer. To do this, we built on top of mature and popular web technologies. + +When you `reflex run` your app, Reflex compiles the frontend down to a single-page [Next.js](https://nextjs.org) app and serves it on a port (by default `3000`) that you can access in your browser. + +The frontend's job is to reflect the app's state, and send events to the backend when the user interacts with the UI. No actual logic is run on the frontend. + +### Components + +Reflex frontends are built using components that can be composed together to create complex UIs. Instead of using a templating language that mixes HTML and Python, we just use Python functions to define the UI. + +```python +def index(): + return rx.hstack( + rx.link( + rx.avatar(src=GithubState.profile_image), + href=GithubState.url, + ), + rx.input( + placeholder="Your Github username", + on_blur=GithubState.set_profile, + ), + ) +``` + +In our example app, we have components such as `rx.hstack`, `rx.avatar`, and `rx.input`. These components can have different **props** that affect their appearance and functionality - for example the `rx.input` component has a `placeholder` prop to display the default text. + +We can make our components respond to user interactions with events such as `on_blur`, which we will discuss more below. + +Under the hood, these components compile down to React components. For example, the above code compiles down to the following React code: + +```jsx + + + + + + +``` + +Many of our core components are based on [Radix](https://radix-ui.com/), a popular React component library. We also have many other components for graphing, datatables, and more. + +We chose React because it is a popular library with a huge ecosystem. Our goal isn't to recreate the web ecosystem, but to make it accessible to Python developers. + +This also lets our users bring their own components if we don't have a component they need. Users can [wrap their own React components]({wrapping_react.overview.path}) and then [publish them]({custom_components.overview.path}) for others to use. Over time we will build out our [third party component ecosystem]({cc.path}) so that users can easily find and use components that others have built. + +### Styling + +We wanted to make sure Reflex apps look good out of the box, while still giving developers full control over the appearance of their app. + +We have a core [theming system]({styling.theming.path}) that lets you set high level styling options such as dark mode and accent color throughout your app to give it a unified look and feel. + +Beyond this, Reflex components can be styled using the full power of CSS. We leverage the [Emotion](https://emotion.sh/docs/introduction) library to allow "CSS-in-Python" styling, so you can pass any CSS prop as a keyword argument to a component. This includes [responsive props]({styling.responsive.path}) by passing a list of values. + +## Backend + +Now let's look at how we added interactivity to our apps. + +In Reflex only the frontend compiles to Javascript and runs on the user's browser, while all the state and logic stays in Python and is run on the server. When you `reflex run`, we start a FastAPI server (by default on port `8000`) that the frontend connects to through a websocket. + +All the state and logic are defined within a `State` class. + +```python +class GithubState(rx.State): + url: str = "https://github.com/reflex-dev" + profile_image: str = "https://avatars.githubusercontent.com/u/104714959" + + def set_profile(self, username: str): + if username == "": + return + github_data = requests.get(f"https://api.github.com/users/\{username}").json() + self.url = github_data["url"] + self.profile_image = github_data["avatar_url"] +``` + +The state is made up of **vars** and **event handlers**. + +Vars are any values in your app that can change over time. They are defined as class attributes on your `State` class, and may be any Python type that can be serialized to JSON. In our example, `url` and `profile_image` are vars. + +Event handlers are methods in your `State` class that are called when the user interacts with the UI. They are the only way that we can modify the vars in Reflex, and can be called in response to user actions, such as clicking a button or typing in a text box. In our example, `set_profile` is an event handler that updates the `url` and `profile_image` vars. + +Since event handlers are run on the backend, you can use any Python library within them. In our example, we use the `requests` library to make an API call to Github to get the user's profile image. + +## Event Processing + +Now we get into the interesting part - how we handle events and state updates. + +Normally when writing web apps, you have to write a lot of boilerplate code to connect the frontend and backend. With Reflex, you don't have to worry about that - we handle the communication between the frontend and backend for you. Developers just have to write their event handler logic, and when the vars are updated the UI is automatically updated. + +You can refer to the diagram above for a visual representation of the process. Let's walk through it with our Github profile image example. + +### Event Triggers + +The user can interact with the UI in many ways, such as clicking a button, typing in a text box, or hovering over an element. In Reflex, we call these **event triggers**. + +```python +rx.input( + placeholder="Your Github username", + on_blur=GithubState.set_profile, +) +``` + +In our example we bind the `on_blur` event trigger to the `set_profile` event handler. This means that when the user types in the input field and then clicks away, the `set_profile` event handler is called. + +### Event Queue + +On the frontend, we maintain an event queue of all pending events. An event consists of three major pieces of data: + +- **client token**: Each client (browser tab) has a unique token to identify it. This let's the backend know which state to update. +- **event handler**: The event handler to run on the state. +- **arguments**: The arguments to pass to the event handler. + +Let's assume I type my username "picklelo" into the input. In this example, our event would look something like this: + +```json +{ + "client_token": "abc123", + "event_handler": "GithubState.set_profile", + "arguments": ["picklelo"] +} +``` + +On the frontend, we maintain an event queue of all pending events. + +When an event is triggered, it is added to the queue. We have a `processing` flag to make sure only one event is processed at a time. This ensures that the state is always consistent and there aren't any race conditions with two event handlers modifying the state at the same time. + +```md alert info +# There are exceptions to this, such as [background events]({events.background_events.path}) which allow you to run events in the background without blocking the UI. +``` + +Once the event is ready to be processed, it is sent to the backend through a WebSocket connection. + +### State Manager + +Once the event is received, it is processed on the backend. + +Reflex uses a **state manager** which maintains a mapping between client tokens and their state. By default, the state manager is just an in-memory dictionary, but it can be extended to use a database or cache. In production we use Redis as our state manager. + +### Event Handling + +Once we have the user's state, the next step is to run the event handler with the arguments. + +```python + def set_profile(self, username: str): + if username == "": + return + github_data = requests.get(f"https://api.github.com/users/\{username}").json() + self.url = github_data["url"] + self.profile_image = github_data["avatar_url"] +``` + +In our example, the `set_profile` event handler is run on the user's state. This makes an API call to Github to get the user's profile image, and then updates the state's `url` and `profile_image` vars. + +### State Updates + +Every time an event handler returns (or [yields]({events.yield_events.path})), we save the state in the state manager and send the **state updates** to the frontend to update the UI. + +To maintain performance as your state grows, internally Reflex keeps track of vars that were updated during the event handler (**dirty vars**). When the event handler is done processing, we find all the dirty vars and create a state update to send to the frontend. + +In our case, the state update may look something like this: + +```json +{ + "url": "https://github.com/picklelo", + "profile_image": "https://avatars.githubusercontent.com/u/104714959" +} +``` + +We store the new state in our state manager, and then send the state update to the frontend. The frontend then updates the UI to reflect the new state. In our example, the new Github profile image is displayed. diff --git a/docs/api-reference/browser_javascript.md b/docs/api-reference/browser_javascript.md new file mode 100644 index 00000000000..7321653d108 --- /dev/null +++ b/docs/api-reference/browser_javascript.md @@ -0,0 +1,224 @@ +```python exec +import asyncio +from typing import Any +import reflex as rx +from pcweb.pages.docs import wrapping_react +from pcweb.pages.docs import library +``` + +# Browser Javascript + +Reflex compiles your frontend code, defined as python functions, into a Javascript web application +that runs in the user's browser. There are instances where you may need to supply custom javascript +code to interop with Web APIs, use certain third-party libraries, or wrap low-level functionality +that is not exposed via Reflex's Python API. + +```md alert +# Avoid Custom Javascript + +Custom Javascript code in your Reflex app presents a maintenance challenge, as it will be harder to debug and may be unstable across Reflex versions. + +Prefer to use the Python API whenever possible and file an issue if you need additional functionality that is not currently provided. +``` + +## Executing Script + +There are four ways to execute custom Javascript code into your Reflex app: + +- `rx.script` - Injects the script via `next/script` for efficient loading of inline and external Javascript code. Described further in the [component library]({library.other.script.path}). + - These components can be directly included in the body of a page, or they may + be passed to `rx.App(head_components=[rx.script(...)])` to be included in + the `` tag of all pages. +- `rx.call_script` - An event handler that evaluates arbitrary Javascript code, + and optionally returns the result to another event handler. + +These previous two methods can work in tandem to load external scripts and then +call functions defined within them in response to user events. + +The following two methods are geared towards wrapping components and are +described with examples in the [Wrapping React]({wrapping_react.overview.path}) +section. + +- `_get_hooks` and `_get_custom_code` in an `rx.Component` subclass +- `Var.create` with `_var_is_local=False` + +## Inline Scripts + +The `rx.script` component is the recommended way to load inline Javascript for greater control over +frontend behavior. + +The functions and variables in the script can be accessed from backend event +handlers or frontend event triggers via the `rx.call_script` interface. + +```python demo exec +class SoundEffectState(rx.State): + @rx.event(background=True) + async def delayed_play(self): + await asyncio.sleep(1) + return rx.call_script("playFromStart(button_sfx)") + + +def sound_effect_demo(): + return rx.hstack( + rx.script(""" + var button_sfx = new Audio("/vintage-button-sound-effect.mp3") + function playFromStart (sfx) {sfx.load(); sfx.play()}"""), + rx.button("Play Immediately", on_click=rx.call_script("playFromStart(button_sfx)")), + rx.button("Play Later", on_click=SoundEffectState.delayed_play), + ) +``` + +## External Scripts + +External scripts can be loaded either from the `assets` directory, or from CDN URL, and then controlled +via `rx.call_script`. + +```python demo +rx.vstack( + rx.script( + src="https://cdn.jsdelivr.net/gh/scottschiller/snowstorm@snowstorm_20131208/snowstorm-min.js", + ), + rx.script(""" + window.addEventListener('load', function() { + if (typeof snowStorm !== 'undefined') { + snowStorm.autoStart = false; + snowStorm.snowColor = '#111'; + } + }); + """), + rx.button("Start Duststorm", on_click=rx.call_script("snowStorm.start()")), + rx.button("Toggle Duststorm", on_click=rx.call_script("snowStorm.toggleSnow()")), +) +``` + +## Accessing Client Side Values + +The `rx.call_script` function accepts a `callback` parameter that expects an +Event Handler with one argument which will receive the result of evaluating the +Javascript code. This can be used to access client-side values such as the +`window.location` or current scroll location, or any previously defined value. + +```python demo exec +class WindowState(rx.State): + location: dict[str, str] = {} + scroll_position: dict[str, int] = {} + + def update_location(self, location): + self.location = location + + def update_scroll_position(self, scroll_position): + self.scroll_position = { + "x": scroll_position[0], + "y": scroll_position[1], + } + + @rx.event + def get_client_values(self): + return [ + rx.call_script( + "window.location", + callback=WindowState.update_location + ), + rx.call_script( + "[window.scrollX, window.scrollY]", + callback=WindowState.update_scroll_position, + ), + ] + + +def window_state_demo(): + return rx.vstack( + rx.button("Update Values", on_click=WindowState.get_client_values), + rx.text(f"Scroll Position: {WindowState.scroll_position.to_string()}"), + rx.text("window.location:"), + rx.text_area(value=WindowState.location.to_string(), is_read_only=True), + on_mount=WindowState.get_client_values, + ) +``` + +```md alert +# Allowed Callback Values + +The `callback` parameter may be an `EventHandler` with one argument, or a lambda with one argument that returns an `EventHandler`. +If the callback is None, then no event is triggered. +``` + +## Using React Hooks + +To use React Hooks directly in a Reflex app, you must subclass `rx.Component`, +typically `rx.Fragment` is used when the hook functionality has no visual +element. The hook code is returned by the `add_hooks` method, which is expected +to return a `list[str]` containing Javascript code which will be inserted into the +page component (i.e the render function itself). + +For supporting code that must be defined outside of the component render +function, use `_get_custom_code`. + +The following example uses `useEffect` to register global hotkeys on the +`document` object, and then triggers an event when a specific key is pressed. + +```python demo exec +import dataclasses + +from reflex.utils import imports + +@dataclasses.dataclass +class KeyEvent: + """Interface of Javascript KeyboardEvent""" + key: str = "" + +def key_event_spec(ev: rx.Var[KeyEvent]) -> tuple[rx.Var[str]]: + # Takes the event object and returns the key pressed to send to the state + return (ev.key,) + +class GlobalHotkeyState(rx.State): + key: str = "" + + @rx.event + def update_key(self, key): + self.key = key + + +class GlobalHotkeyWatcher(rx.Fragment): + """A component that listens for key events globally.""" + + # The event handler that will be called + on_key_down: rx.EventHandler[key_event_spec] + + def add_imports(self) -> imports.ImportDict: + """Add the imports for the component.""" + return { + "react": [imports.ImportVar(tag="useEffect")], + } + + def add_hooks(self) -> list[str | rx.Var]: + """Add the hooks for the component.""" + return [ + """ + useEffect(() => { + const handle_key = %s; + document.addEventListener("keydown", handle_key, false); + return () => { + document.removeEventListener("keydown", handle_key, false); + } + }) + """ + % str(rx.Var.create(self.event_triggers["on_key_down"])) + ] + +def global_key_demo(): + return rx.vstack( + GlobalHotkeyWatcher.create( + keys=["a", "s", "d", "w"], + on_key_down=lambda key: rx.cond( + rx.Var.create(["a", "s", "d", "w"]).contains(key), + GlobalHotkeyState.update_key(key), + rx.console_log(key) + ) + ), + rx.text("Press a, s, d or w to trigger an event"), + rx.heading(f"Last watched key pressed: {GlobalHotkeyState.key}"), + ) +``` + +This snippet can also be imported through pip: [reflex-global-hotkey](https://pypi.org/project/reflex-global-hotkey/). diff --git a/docs/api-reference/browser_storage.md b/docs/api-reference/browser_storage.md new file mode 100644 index 00000000000..04374f9a4aa --- /dev/null +++ b/docs/api-reference/browser_storage.md @@ -0,0 +1,336 @@ +# Browser Storage + +## rx.Cookie + +Represents a state Var that is stored as a cookie in the browser. Currently only supports string values. + +Parameters + +- `name` : The name of the cookie on the client side. +- `path`: The cookie path. Use `/` to make the cookie accessible on all pages. +- `max_age` : Relative max age of the cookie in seconds from when the client receives it. +- `domain`: Domain for the cookie (e.g., `sub.domain.com` or `.allsubdomains.com`). +- `secure`: If the cookie is only accessible through HTTPS. +- `same_site`: Whether the cookie is sent with third-party requests. Can be one of (`True`, `False`, `None`, `lax`, `strict`). + +```python +class CookieState(rx.State): + c1: str = rx.Cookie() + c2: str = rx.Cookie('c2 default') + + # cookies with custom settings + c3: str = rx.Cookie(max_age=2) # expires after 2 second + c4: str = rx.Cookie(same_site='strict') + c5: str = rx.Cookie(path='/foo/') # only accessible on `/foo/` + c6: str = rx.Cookie(name='c6-custom-name') +``` + +```md alert warning +# **The default value of a Cookie is never set in the browser!** + +The Cookie value is only set when the Var is assigned. If you need to set a +default value, you can assign a value to the cookie in an `on_load` event +handler. +``` + +## Accessing Cookies + +Cookies are accessed like any other Var in the state. If another state needs access +to the value of a cookie, the state should be a substate of the state that defines +the cookie. Alternatively the `get_state` API can be used to access the other state. + +For rendering cookies in the frontend, import the state that defines the cookie and +reference it directly. + +```md alert warning +# **Two separate states should _avoid_ defining `rx.Cookie` with the same name.** + +Although it is technically possible, the cookie options may differ, leading to +unexpected results. + +Additionally, updating the cookie value in one state will not automatically +update the value in the other state without a page refresh or navigation event. +``` + +## rx.remove_cookies + +Remove a cookie from the client's browser. + +Parameters: + +- `key`: The name of cookie to remove. + +```python +rx.button( + 'Remove cookie', on_click=rx.remove_cookie('key') +) +``` + +This event can also be returned from an event handler: + +```python +class CookieState(rx.State): + ... + def logout(self): + return rx.remove_cookie('auth_token') +``` + +## rx.LocalStorage + +Represents a state Var that is stored in localStorage in the browser. Currently only supports string values. + +Parameters + +- `name`: The name of the storage key on the client side. +- `sync`: Boolean indicates if the state should be kept in sync across tabs of the same browser. + +```python +class LocalStorageState(rx.State): + # local storage with default settings + l1: str = rx.LocalStorage() + + # local storage with custom settings + l2: str = rx.LocalStorage("l2 default") + l3: str = rx.LocalStorage(name="l3") + + # local storage that automatically updates in other states across tabs + l4: str = rx.LocalStorage(sync=True) +``` + +### Syncing Vars + +Because LocalStorage applies to the entire browser, all LocalStorage Vars are +automatically shared across tabs. + +The `sync` parameter controls whether an update in one tab should be actively +propagated to other tabs without requiring a navigation or page refresh event. + +## rx.remove_local_storage + +Remove a local storage item from the client's browser. + +Parameters + +- `key`: The key to remove from local storage. + +```python +rx.button( + 'Remove Local Storage', + on_click=rx.remove_local_storage('key'), +) +``` + +This event can also be returned from an event handler: + +```python +class LocalStorageState(rx.State): + ... + def logout(self): + return rx.remove_local_storage('local_storage_state.l1') +``` + +## rx.clear_local_storage() + +Clear all local storage items from the client's browser. This may affect other +apps running in the same domain or libraries within your app that use local +storage. + +```python +rx.button( + 'Clear all Local Storage', + on_click=rx.clear_local_storage(), +) +``` + +## rx.SessionStorage + +Represents a state Var that is stored in sessionStorage in the browser. Similar to localStorage, but the data is cleared when the page session ends (when the browser/tab is closed). Currently only supports string values. + +Parameters + +- `name`: The name of the storage key on the client side. + +```python +class SessionStorageState(rx.State): + # session storage with default settings + s1: str = rx.SessionStorage() + + # session storage with custom settings + s2: str = rx.SessionStorage("s2 default") + s3: str = rx.SessionStorage(name="s3") +``` + +### Session Persistence + +SessionStorage data is cleared when the page session ends. A page session lasts as long as the browser is open and survives page refreshes and restores, but is cleared when the tab or browser is closed. + +Unlike LocalStorage, SessionStorage is isolated to the tab/window in which it was created, so it's not shared with other tabs/windows of the same origin. + +## rx.remove_session_storage + +Remove a session storage item from the client's browser. + +Parameters + +- `key`: The key to remove from session storage. + +```python +rx.button( + 'Remove Session Storage', + on_click=rx.remove_session_storage('key'), +) +``` + +This event can also be returned from an event handler: + +```python +class SessionStorageState(rx.State): + ... + def logout(self): + return rx.remove_session_storage('session_storage_state.s1') +``` + +## rx.clear_session_storage() + +Clear all session storage items from the client's browser. This may affect other +apps running in the same domain or libraries within your app that use session +storage. + +```python +rx.button( + 'Clear all Session Storage', + on_click=rx.clear_session_storage(), +) +``` + +# Serialization Strategies + +If a non-trivial data structure should be stored in a `Cookie`, `LocalStorage`, or `SessionStorage` var it needs to be serialized before and after storing it. It is recommended to use a pydantic class for the data which provides simple serialization helpers and works recursively in complex object structures. + +```python demo exec +import reflex as rx +import pydantic + + +class AppSettings(pydantic.BaseModel): + theme: str = 'light' + sidebar_visible: bool = True + update_frequency: int = 60 + error_messages: list[str] = pydantic.Field(default_factory=list) + + +class ComplexLocalStorageState(rx.State): + data_raw: str = rx.LocalStorage("{}") + data: AppSettings = AppSettings() + settings_open: bool = False + + @rx.event + def save_settings(self): + self.data_raw = self.data.model_dump_json() + self.settings_open = False + + @rx.event + def open_settings(self): + self.data = AppSettings.model_validate_json(self.data_raw) + self.settings_open = True + + @rx.event + def set_field(self, field, value): + setattr(self.data, field, value) + + +def app_settings(): + return rx.form.root( + rx.foreach( + ComplexLocalStorageState.data.error_messages, + rx.text, + ), + rx.form.field( + rx.flex( + rx.form.label( + "Theme", + rx.input( + value=ComplexLocalStorageState.data.theme, + on_change=lambda v: ComplexLocalStorageState.set_field( + "theme", v + ), + ), + ), + rx.form.label( + "Sidebar Visible", + rx.switch( + checked=ComplexLocalStorageState.data.sidebar_visible, + on_change=lambda v: ComplexLocalStorageState.set_field( + "sidebar_visible", + v, + ), + ), + ), + rx.form.label( + "Update Frequency (seconds)", + rx.input( + value=ComplexLocalStorageState.data.update_frequency, + on_change=lambda v: ComplexLocalStorageState.set_field( + "update_frequency", + v, + ), + ), + ), + rx.dialog.close(rx.button("Save", type="submit")), + gap=2, + direction="column", + ) + ), + on_submit=lambda _: ComplexLocalStorageState.save_settings(), + ) + +def app_settings_example(): + return rx.dialog.root( + rx.dialog.trigger( + rx.button("App Settings", on_click=ComplexLocalStorageState.open_settings), + ), + rx.dialog.content( + rx.dialog.title("App Settings"), + app_settings(), + ), + ) +``` + +# Comparison of Storage Types + +Here's a comparison of the different client-side storage options in Reflex: + +| Feature | rx.Cookie | rx.LocalStorage | rx.SessionStorage | +| ------------------- | --------------------------------- | --------------------------- | --------------------------- | +| Persistence | Until cookie expires | Until explicitly deleted | Until browser/tab is closed | +| Storage Limit | ~4KB | ~5MB | ~5MB | +| Sent with Requests | Yes | No | No | +| Accessibility | Server & Client | Client Only | Client Only | +| Expiration | Configurable | Never | End of session | +| Scope | Configurable (domain, path) | Origin (domain) | Tab/Window | +| Syncing Across Tabs | No | Yes (with sync=True) | No | +| Use Case | Authentication, Server-side state | User preferences, App state | Temporary session data | + +# When to Use Each Storage Type + +## Use rx.Cookie When: + +- You need the data to be accessible on the server side (cookies are sent with HTTP requests) +- You're handling user authentication +- You need fine-grained control over expiration and scope +- You need to limit the data to specific paths in your app + +## Use rx.LocalStorage When: + +- You need to store larger amounts of data (up to ~5MB) +- You want the data to persist indefinitely (until explicitly deleted) +- You need to share data between different tabs/windows of your app +- You want to store user preferences that should be remembered across browser sessions + +## Use rx.SessionStorage When: + +- You need temporary data that should be cleared when the browser/tab is closed +- You want to isolate data to a specific tab/window +- You're storing sensitive information that shouldn't persist after the session ends +- You're implementing per-session features like form data, shopping carts, or multi-step processes +- You want to persist data for a state after Redis expiration (for server-side state that needs to survive longer than Redis TTL) diff --git a/docs/api-reference/cli.md b/docs/api-reference/cli.md new file mode 100644 index 00000000000..76da441c036 --- /dev/null +++ b/docs/api-reference/cli.md @@ -0,0 +1,128 @@ +# CLI + +The `reflex` command line interface (CLI) is a tool for creating and managing Reflex apps. + +To see a list of all available commands, run `reflex --help`. + +```bash +$ reflex --help + +Usage: reflex [OPTIONS] COMMAND [ARGS]... + + Reflex CLI to create, run, and deploy apps. + +Options: + --version Show the version and exit. + --help Show this message and exit. + +Commands: + cloud The Hosting CLI. + component CLI for creating custom components. + db Subcommands for managing the database schema. + deploy Deploy the app to the Reflex hosting service. + export Export the app to a zip file. + init Initialize a new Reflex app in the current directory. + login Authenticate with experimental Reflex hosting service. + logout Log out of access to Reflex hosting service. + rename Rename the app in the current directory. + run Run the app in the current directory. + script Subcommands for running helper scripts. +``` + +## Init + +The `reflex init` command creates a new Reflex app in the current directory. +If an `rxconfig.py` file already exists already, it will re-initialize the app with the latest template. + +```bash +$ reflex init --help +Usage: reflex init [OPTIONS] + + Initialize a new Reflex app in the current directory. + +Options: + --name APP_NAME The name of the app to initialize. + --template [demo|sidebar|blank] + The template to initialize the app with. + --loglevel [debug|info|warning|error|critical] + The log level to use. [default: + LogLevel.INFO] + --help Show this message and exit. +``` + +## Run + +The `reflex run` command runs the app in the current directory. + +By default it runs your app in development mode. +This means that the app will automatically reload when you make changes to the code. +You can also run in production mode which will create an optimized build of your app. + +You can configure the mode, as well as other options through flags. + +```bash +$ reflex run --help +Usage: reflex run [OPTIONS] + + Run the app in the current directory. + +Options: + --env [dev|prod] The environment to run the app in. + [default: Env.DEV] + --frontend-only Execute only frontend. + --backend-only Execute only backend. + --frontend-port TEXT Specify a different frontend port. + [default: 3000] + --backend-port TEXT Specify a different backend port. [default: + 8000] + --backend-host TEXT Specify the backend host. [default: + 0.0.0.0] + --loglevel [debug|info|warning|error|critical] + The log level to use. [default: + LogLevel.INFO] + --help Show this message and exit. +``` + +## Export + +You can export your app's frontend and backend to zip files using the `reflex export` command. + +The frontend is a compiled NextJS app, which can be deployed to a static hosting service like Github Pages or Vercel. +However this is just a static build, so you will need to deploy the backend separately. +See the self-hosting guide for more information. + +## Rename + +The `reflex rename` command allows you to rename your Reflex app. This updates the app name in the configuration files. + +```bash +$ reflex rename --help +Usage: reflex rename [OPTIONS] NEW_NAME + + Rename the app in the current directory. + +Options: + --loglevel [debug|default|info|warning|error|critical] + The log level to use. + --help Show this message and exit. +``` + +## Cloud + +The `reflex cloud` command provides access to the Reflex Cloud hosting service. It includes subcommands for managing apps, projects, secrets, and more. + +For detailed documentation on Reflex Cloud and deployment, see the [Cloud Quick Start Guide](https://reflex.dev/docs/hosting/deploy-quick-start/). + +## Script + +The `reflex script` command provides access to helper scripts for Reflex development. + +```bash +$ reflex script --help +Usage: reflex script [OPTIONS] COMMAND [ARGS]... + + Subcommands for running helper scripts. + +Options: + --help Show this message and exit. +``` diff --git a/docs/api-reference/event_triggers.md b/docs/api-reference/event_triggers.md new file mode 100644 index 00000000000..4a9620d99f5 --- /dev/null +++ b/docs/api-reference/event_triggers.md @@ -0,0 +1,390 @@ +```python exec +from datetime import datetime + +import reflex as rx + +from pcweb.templates.docpage import docdemo, h1_comp, text_comp, docpage +from pcweb.pages.docs import events + +SYNTHETIC_EVENTS = [ + { + "name": "on_focus", + "description": "The on_focus event handler is called when the element (or some element inside of it) receives focus. For example, it’s called when the user clicks on a text input.", + "state": """class FocusState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self, text): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.input(value=FocusState.text, on_focus=FocusState.change_text)""", + }, + { + "name": "on_blur", + "description": "The on_blur event handler is called when focus has left the element (or left some element inside of it). For example, it’s called when the user clicks outside of a focused text input.", + "state": """class BlurState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self, text): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.input(value=BlurState.text, on_blur=BlurState.change_text)""", + }, + { + "name": "on_change", + "description": "The on_change event handler is called when the value of an element has changed. For example, it’s called when the user types into a text input each keystroke triggers the on change.", + "state": """class ChangeState(rx.State): + checked: bool = False + + @rx.event + def set_checked(self): + self.checked = not self.checked + +""", + "example": """rx.switch(on_change=ChangeState.set_checked)""", + }, + { + "name": "on_click", + "description": "The on_click event handler is called when the user clicks on an element. For example, it’s called when the user clicks on a button.", + "state": """class ClickState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(ClickState.text, on_click=ClickState.change_text)""", + }, + { + "name": "on_context_menu", + "description": "The on_context_menu event handler is called when the user right-clicks on an element. For example, it’s called when the user right-clicks on a button.", + "state": """class ContextState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(ContextState.text, on_context_menu=ContextState.change_text)""", + }, + { + "name": "on_double_click", + "description": "The on_double_click event handler is called when the user double-clicks on an element. For example, it’s called when the user double-clicks on a button.", + "state": """class DoubleClickState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(DoubleClickState.text, on_double_click=DoubleClickState.change_text)""", + }, + { + "name": "on_mount", + "description": "The on_mount event handler is called after the component is rendered on the page. It is similar to a page on_load event, although it does not necessarily fire when navigating between pages. This event is particularly useful for initializing data, making API calls, or setting up component-specific state when a component first appears.", + "state": """class MountState(rx.State): + events: list[str] = [] + data: list[dict] = [] + loading: bool = False + + @rx.event + def on_mount(self): + self.events = self.events[-4:] + ["on_mount @ " + str(datetime.now())] + + @rx.event + async def load_data(self): + # Common pattern: Set loading state, yield to update UI, then fetch data + self.loading = True + yield + # Simulate API call + import asyncio + await asyncio.sleep(1) + self.data = [dict(id=1, name="Item 1"), dict(id=2, name="Item 2")] + self.loading = False +""", + "example": """rx.vstack( + rx.heading("Component Lifecycle Demo"), + rx.foreach(MountState.events, rx.text), + rx.cond( + MountState.loading, + rx.spinner(), + rx.foreach( + MountState.data, + lambda item: rx.text(f"ID: {item['id']} - {item['name']}") + ) + ), + on_mount=MountState.on_mount, +)""", + }, + { + "name": "on_unmount", + "description": "The on_unmount event handler is called after removing the component from the page. However, on_unmount will only be called for internal navigation, not when following external links or refreshing the page. This event is useful for cleaning up resources, saving state, or performing cleanup operations before a component is removed from the DOM.", + "state": """class UnmountState(rx.State): + events: list[str] = [] + resource_id: str = "resource-12345" + status: str = "Resource active" + + @rx.event + def on_unmount(self): + self.events = self.events[-4:] + ["on_unmount @ " + str(datetime.now())] + # Common pattern: Clean up resources when component is removed + self.status = f"Resource {self.resource_id} cleaned up" + + @rx.event + def initialize_resource(self): + self.status = f"Resource {self.resource_id} initialized" +""", + "example": """rx.vstack( + rx.heading("Unmount Demo"), + rx.foreach(UnmountState.events, rx.text), + rx.text(UnmountState.status), + rx.link( + rx.button("Navigate Away (Triggers Unmount)"), + href="/docs", + ), + on_mount=UnmountState.initialize_resource, + on_unmount=UnmountState.on_unmount, +)""", + }, + { + "name": "on_mouse_up", + "description": "The on_mouse_up event handler is called when the user releases a mouse button on an element. For example, it’s called when the user releases the left mouse button on a button.", + "state": """class MouseUpState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseUpState.text, on_mouse_up=MouseUpState.change_text)""", + }, + { + "name": "on_mouse_down", + "description": "The on_mouse_down event handler is called when the user presses a mouse button on an element. For example, it’s called when the user presses the left mouse button on a button.", + "state": """class MouseDown(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseDown.text, on_mouse_down=MouseDown.change_text)""", + }, + { + "name": "on_mouse_enter", + "description": "The on_mouse_enter event handler is called when the user’s mouse enters an element. For example, it’s called when the user’s mouse enters a button.", + "state": """class MouseEnter(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseEnter.text, on_mouse_enter=MouseEnter.change_text)""", + }, + { + "name": "on_mouse_leave", + "description": "The on_mouse_leave event handler is called when the user’s mouse leaves an element. For example, it’s called when the user’s mouse leaves a button.", + "state": """class MouseLeave(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseLeave.text, on_mouse_leave=MouseLeave.change_text)""", + }, + { + "name": "on_mouse_move", + "description": "The on_mouse_move event handler is called when the user moves the mouse over an element. For example, it’s called when the user moves the mouse over a button.", + "state": """class MouseMove(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseMove.text, on_mouse_move=MouseMove.change_text)""", + }, + { + "name": "on_mouse_out", + "description": "The on_mouse_out event handler is called when the user’s mouse leaves an element. For example, it’s called when the user’s mouse leaves a button.", + "state": """class MouseOut(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseOut.text, on_mouse_out=MouseOut.change_text)""", + }, + { + "name": "on_mouse_over", + "description": "The on_mouse_over event handler is called when the user’s mouse enters an element. For example, it’s called when the user’s mouse enters a button.", + "state": """class MouseOver(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.button(MouseOver.text, on_mouse_over=MouseOver.change_text)""", + }, + { + "name": "on_scroll", + "description": "The on_scroll event handler is called when the user scrolls the page. For example, it’s called when the user scrolls the page down.", + "state": """class ScrollState(rx.State): + text = "Change Me!" + + @rx.event + def change_text(self): + if self.text == "Change Me!": + self.text = "Changed!" + else: + self.text = "Change Me!" +""", + "example": """rx.vstack( + rx.text("Scroll to make the text below change."), + rx.text(ScrollState.text), + rx.text("Scroll to make the text above change."), + on_scroll=ScrollState.change_text, + overflow = "auto", + height = "3em", + width = "100%", + )""", + }, +] +for i in SYNTHETIC_EVENTS: + exec(i["state"]) + +def component_grid(): + events = [] + for event in SYNTHETIC_EVENTS: + events.append( + rx.vstack( + h1_comp(text=event["name"]), + text_comp(text=event["description"]), + docdemo( + event["example"], state=event["state"], comp=eval(event["example"]) + ), + align_items="left", + ) + ) + + return rx.box(*events) +``` + +# Event Triggers + +Components can modify the state based on user events such as clicking a button or entering text in a field. +These events are triggered by event triggers. + +Event triggers are component specific and are listed in the documentation for each component. + +## Component Lifecycle Events + +Reflex components have lifecycle events like `on_mount` and `on_unmount` that allow you to execute code at specific points in a component's existence. These events are crucial for initializing data, cleaning up resources, and creating dynamic user interfaces. + +### When Lifecycle Events Are Activated + +- **on_mount**: This event is triggered immediately after a component is rendered and attached to the DOM. It fires: + - When a page containing the component is first loaded + - When a component is conditionally rendered (appears after being hidden) + - When navigating to a page containing the component using internal navigation + - It does NOT fire when the page is refreshed or when following external links + +- **on_unmount**: This event is triggered just before a component is removed from the DOM. It fires: + - When navigating away from a page containing the component using internal navigation + - When a component is conditionally removed from the DOM (e.g., via a condition that hides it) + - It does NOT fire when refreshing the page, closing the browser tab, or following external links + +## Page Load Events + +In addition to component lifecycle events, Reflex also provides page-level events like `on_load` that are triggered when a page loads. The `on_load` event is useful for: + +- Fetching data when a page first loads +- Checking authentication status +- Initializing page-specific state +- Setting default values for cookies or browser storage + +You can specify an event handler to run when the page loads using the `on_load` parameter in the `@rx.page` decorator or `app.add_page()` method: + +```python +class State(rx.State): + data: dict = dict() + + @rx.event + def get_data(self): + # Fetch data when the page loads + self.data = fetch_data() + +@rx.page(on_load=State.get_data) +def index(): + return rx.text('Data loaded on page load') +``` + +This is particularly useful for authentication checks: + +```python +class State(rx.State): + authenticated: bool = False + + @rx.event + def check_auth(self): + # Check if user is authenticated + self.authenticated = check_auth() + if not self.authenticated: + return rx.redirect('/login') + +@rx.page(on_load=State.check_auth) +def protected_page(): + return rx.text('Protected content') +``` + +For more details on page load events, see the [page load events documentation]({events.page_load_events.path}). + +# Event Reference + +```python eval +rx.box( + rx.divider(), + component_grid(), +) +``` diff --git a/docs/api-reference/plugins.md b/docs/api-reference/plugins.md new file mode 100644 index 00000000000..b05263ef359 --- /dev/null +++ b/docs/api-reference/plugins.md @@ -0,0 +1,242 @@ +```python exec +import reflex as rx +from pcweb.pages.docs import advanced_onboarding +``` + +# Plugins + +Reflex supports a plugin system that allows you to extend the framework's functionality during the compilation process. Plugins can add frontend dependencies, modify build configurations, generate static assets, and perform custom tasks before compilation. + +## Configuring Plugins + +Plugins are configured in your `rxconfig.py` file using the `plugins` parameter: + +```python +import reflex as rx + +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.SitemapPlugin(), + rx.plugins.TailwindV4Plugin(), + ], +) +``` + +## Built-in Plugins + +Reflex comes with several built-in plugins that provide common functionality. + +### SitemapPlugin + +The `SitemapPlugin` automatically generates a sitemap.xml file for your application, which helps search engines discover and index your pages. + +```python +import reflex as rx + +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.SitemapPlugin(), + ], +) +``` + +The sitemap plugin automatically includes all your app's routes. For dynamic routes or custom configuration, you can add sitemap metadata to individual pages: + +```python +@rx.page(route="/blog/[slug]", context={"sitemap": {"changefreq": "weekly", "priority": 0.8}}) +def blog_post(): + return rx.text("Blog post content") + +@rx.page(route="/about", context={"sitemap": {"changefreq": "monthly", "priority": 0.5}}) +def about(): + return rx.text("About page") +``` + +The sitemap configuration supports the following options: + +- `loc`: Custom URL for the page (required for dynamic routes) +- `lastmod`: Last modification date (datetime object) +- `changefreq`: How frequently the page changes (`"always"`, `"hourly"`, `"daily"`, `"weekly"`, `"monthly"`, `"yearly"`, `"never"`) +- `priority`: Priority of this URL relative to other URLs (0.0 to 1.0) + +### TailwindV4Plugin + +The `TailwindV4Plugin` provides support for Tailwind CSS v4, which is the recommended version for new projects and includes performance improvements and new features. + +```python +import reflex as rx + +# Basic configuration +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.TailwindV4Plugin(), + ], +) +``` + +You can customize the Tailwind configuration by passing a config dictionary: + +```python +import reflex as rx + +tailwind_config = { + "theme": { + "extend": { + "colors": { + "brand": { + "50": "#eff6ff", + "500": "#3b82f6", + "900": "#1e3a8a", + } + } + } + }, + "plugins": ["@tailwindcss/typography"], +} + +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.TailwindV4Plugin(tailwind_config), + ], +) +``` + +### TailwindV3Plugin + +The `TailwindV3Plugin` integrates Tailwind CSS v3 into your Reflex application. While still supported, TailwindV4Plugin is recommended for new projects. + +```python +import reflex as rx + +# Basic configuration +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.TailwindV3Plugin(), + ], +) +``` + +You can customize the Tailwind configuration by passing a config dictionary: + +```python +import reflex as rx + +tailwind_config = { + "theme": { + "extend": { + "colors": { + "primary": "#3b82f6", + "secondary": "#64748b", + } + } + }, + "plugins": ["@tailwindcss/typography", "@tailwindcss/forms"], +} + +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.TailwindV3Plugin(tailwind_config), + ], +) +``` + +## Plugin Management + +### Default Plugins + +Some plugins are enabled by default. Currently, the `SitemapPlugin` is enabled automatically. If you want to disable a default plugin, use the `disable_plugins` parameter: + +```python +import reflex as rx + +config = rx.Config( + app_name="my_app", + disable_plugins=["reflex.plugins.sitemap.SitemapPlugin"], +) +``` + +### Plugin Order + +Plugins are executed in the order they appear in the `plugins` list. This can be important if plugins have dependencies on each other or modify the same files. + +```python +import reflex as rx + +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.TailwindV4Plugin(), # Runs first + rx.plugins.SitemapPlugin(), # Runs second + ], +) +``` + +## Plugin Architecture + +All plugins inherit from the base `Plugin` class and can implement several lifecycle methods: + +```python +class Plugin: + def get_frontend_development_dependencies(self, **context) -> list[str]: + """Get NPM packages required by the plugin for development.""" + return [] + + def get_frontend_dependencies(self, **context) -> list[str]: + """Get NPM packages required by the plugin.""" + return [] + + def get_static_assets(self, **context) -> Sequence[tuple[Path, str | bytes]]: + """Get static assets required by the plugin.""" + return [] + + def get_stylesheet_paths(self, **context) -> Sequence[str]: + """Get paths to stylesheets required by the plugin.""" + return [] + + def pre_compile(self, **context) -> None: + """Called before compilation to perform custom tasks.""" + pass +``` + +### Creating Custom Plugins + +You can create custom plugins by inheriting from the base `Plugin` class: + +```python +from reflex.plugins.base import Plugin +from pathlib import Path + +class CustomPlugin(Plugin): + def get_frontend_dependencies(self, **context): + return ["my-custom-package@1.0.0"] + + def pre_compile(self, **context): + # Custom logic before compilation + print("Running custom plugin logic...") + + # Add a custom task + context["add_save_task"](self.create_custom_file) + + def create_custom_file(self): + return "public/custom.txt", "Custom content" +``` + +Then use it in your configuration: + +```python +import reflex as rx +from my_plugins import CustomPlugin + +config = rx.Config( + app_name="my_app", + plugins=[ + CustomPlugin(), + ], +) +``` diff --git a/docs/api-reference/special_events.md b/docs/api-reference/special_events.md new file mode 100644 index 00000000000..675f465ca70 --- /dev/null +++ b/docs/api-reference/special_events.md @@ -0,0 +1,123 @@ +```python exec +import reflex as rx +``` + +# Special Events + +Reflex includes a set of built-in special events that can be utilized as event triggers +or returned from event handlers in your applications. These events enhance interactivity and user experience. +Below are the special events available in Reflex, along with explanations of their functionality: + +## rx.console_log + +Perform a console.log in the browser's console. + +```python demo +rx.button('Log', on_click=rx.console_log('Hello World!')) +``` + +When triggered, this event logs a specified message to the browser's developer console. +It's useful for debugging and monitoring the behavior of your application. + +## rx.scroll_to + +scroll to an element in the page + +```python demo +rx.button( + "Scroll to download button", + on_click=rx.scroll_to("download button") + +) +``` + +When this is triggered, it scrolls to an element passed by id as parameter. Click on button to scroll to download button (rx.download section) at the bottom of the page + +## rx.redirect + +Redirect the user to a new path within the application. + +### Parameters + +- `path`: The destination path or URL to which the user should be redirected. +- `external`: If set to True, the redirection will open in a new tab. Defaults to `False`. + +```python demo +rx.vstack( + rx.button("open in tab", on_click=rx.redirect("/docs/api-reference/special-events")), + rx.button("open in new tab", on_click=rx.redirect('https://github.com/reflex-dev/reflex/', is_external=True)) +) +``` + +When this event is triggered, it navigates the user to a different page or location within your Reflex application. +By default, the redirection occurs in the same tab. However, if you set the external parameter to True, the redirection +will open in a new tab or window, providing a seamless user experience. + +This event can also be run from an event handler in State. It is necessary to `return` the `rx.redirect()`. + +```python demo exec +class RedirectExampleState(rx.State): + """The app state.""" + + @rx.event + def change_page(self): + return rx.redirect('https://github.com/reflex-dev/reflex/', is_external=True) + +def redirect_example(): + return rx.vstack( + rx.button("Change page in State", on_click=RedirectExampleState.change_page), + ) +``` + +## rx.set_clipboard + +Set the specified text content to the clipboard. + +```python demo +rx.button('Copy "Hello World" to clipboard',on_click=rx.set_clipboard('Hello World'),) +``` + +This event allows you to copy a given text or content to the user's clipboard. +It's handy when you want to provide a "Copy to Clipboard" feature in your application, +allowing users to easily copy information to paste elsewhere. + +## rx.set_value + +Set the value of a specified reference element. + +```python demo +rx.hstack( + rx.input(id='input1'), + rx.button( + 'Erase', on_click=rx.set_value('input1', '') + ), +) +``` + +With this event, you can modify the value of a particular HTML element, typically an input field or another form element. + +## rx.window_alert + +Create a window alert in the browser. + +```python demo +rx.button('Alert', on_click=rx.window_alert('Hello World!')) +``` + +## rx.download + +Download a file at a given path. + +Parameters: + +- `url`: The URL of the file to be downloaded. +- `data`: The data to be downloaded. Should be `str` or `bytes`, `data:` URI, `PIL.Image`, or any state Var (to be converted to JSON). +- `filename`: The desired filename of the downloaded file. + +```md alert +# `url` and `data` args are mutually exclusive, and at least one of them must be provided. +``` + +```python demo +rx.button("Download", on_click=rx.download(url="/reflex_banner.webp", filename="different_name_logo.webp"), id="download button") +``` diff --git a/docs/api-reference/utils.md b/docs/api-reference/utils.md new file mode 100644 index 00000000000..6a42a54b8c9 --- /dev/null +++ b/docs/api-reference/utils.md @@ -0,0 +1,170 @@ +```python exec +import reflex as rx +from pcweb import constants, styles +``` + +# Utility Functions + +Reflex provides utility functions to help with common tasks in your applications. + +## run_in_thread + +The `run_in_thread` function allows you to run a **non-async** function in a separate thread, which is useful for preventing long-running operations from blocking the UI event queue. + +```python +async def run_in_thread(func: Callable) -> Any +``` + +### Parameters + +- `func`: The non-async function to run in a separate thread. + +### Returns + +- The return value of the function. + +### Raises + +- `ValueError`: If the function is an async function. + +### Usage + +```python demo exec id=run_in_thread_demo +import asyncio +import dataclasses +import time +import reflex as rx + + +def quick_blocking_function(): + time.sleep(0.5) + return "Quick task completed successfully!" + + +def slow_blocking_function(): + time.sleep(3.0) + return "This should never be returned due to timeout!" + + +@dataclasses.dataclass +class TaskInfo: + result: str = "No result yet" + status: str = "Idle" + + +class RunInThreadState(rx.State): + tasks: list[TaskInfo] = [] + + @rx.event(background=True) + async def run_quick_task(self): + """Run a quick task that completes within the timeout.""" + async with self: + task_ix = len(self.tasks) + self.tasks.append(TaskInfo(status="Running quick task...")) + task_info = self.tasks[task_ix] + + try: + result = await rx.run_in_thread(quick_blocking_function) + async with self: + task_info.result = result + task_info.status = "Complete" + except Exception as e: + async with self: + task_info.result = f"Error: {str(e)}" + task_info.status = "Failed" + + @rx.event(background=True) + async def run_slow_task(self): + """Run a slow task that exceeds the timeout.""" + async with self: + task_ix = len(self.tasks) + self.tasks.append(TaskInfo(status="Running slow task...")) + task_info = self.tasks[task_ix] + + try: + # Run with a timeout of 1 second (not enough time) + result = await asyncio.wait_for( + rx.run_in_thread(slow_blocking_function), + timeout=1.0, + ) + async with self: + task_info.result = result + task_info.status = "Complete" + except asyncio.TimeoutError: + async with self: + # Warning: even though we stopped waiting for the task, + # it may still be running in thread + task_info.result = "Task timed out after 1 second!" + task_info.status = "Timeout" + except Exception as e: + async with self: + task_info.result = f"Error: {str(e)}" + task_info.status = "Failed" + + +def run_in_thread_example(): + return rx.vstack( + rx.heading("run_in_thread Example", size="3"), + rx.hstack( + rx.button( + "Run Quick Task", + on_click=RunInThreadState.run_quick_task, + color_scheme="green", + ), + rx.button( + "Run Slow Task (exceeds timeout)", + on_click=RunInThreadState.run_slow_task, + color_scheme="red", + ), + ), + rx.vstack( + rx.foreach( + RunInThreadState.tasks.reverse()[:10], + lambda task: rx.hstack( + rx.text(task.status), + rx.spacer(), + rx.text(task.result), + ), + ), + align="start", + width="100%", + ), + width="100%", + align_items="start", + spacing="4", + ) +``` + +### When to Use run_in_thread + +Use `run_in_thread` when you need to: + +1. Execute CPU-bound operations that would otherwise block the event loop +2. Call synchronous libraries that don't have async equivalents +3. Prevent long-running operations from blocking UI responsiveness + +### Example: Processing a Large File + +```python +import reflex as rx +import time + +class FileProcessingState(rx.State): + progress: str = "Ready" + + @rx.event(background=True) + async def process_large_file(self): + async with self: + self.progress = "Processing file..." + + def process_file(): + # Simulate processing a large file + time.sleep(5) + return "File processed successfully!" + + # Save the result to a local variable to avoid blocking the event loop. + result = await rx.run_in_thread(process_file) + async with self: + # Then assign the local result to the state while holding the lock. + self.progress = result +``` diff --git a/docs/api-reference/var_system.md b/docs/api-reference/var_system.md new file mode 100644 index 00000000000..10c2b84de2b --- /dev/null +++ b/docs/api-reference/var_system.md @@ -0,0 +1,82 @@ +# Reflex's Var System + +## Motivation + +Reflex supports some basic operations in state variables on the frontend. +Reflex automatically converts variable operations from Python into a JavaScript equivalent. + +Here's an example of a Reflex conditional in Python that returns "Pass" if the threshold is equal to or greater than 50 and "Fail" otherwise: + +```py +rx.cond( + State.threshold >= 50, + "Pass", + "Fail", +) +``` + +The conditional to roughly the following in Javascript: + +```js +state.threshold >= 50 ? "Pass" : "Fail"; +``` + +## Overview + +Simply put, a `Var` in Reflex represents a Javascript expression. +If the type is known, it can be any of the following: + +- `NumberVar` represents an expression that evaluates to a Javascript `number`. `NumberVar` can support both integers and floating point values +- `BooleanVar` represents a boolean expression. For example: `false`, `3 > 2`. +- `StringVar` represents an expression that evaluates to a string. For example: `'hello'`, `(2).toString()`. +- `ArrayVar` represents an expression that evaluates to an array object. For example: `[1, 2, 3]`, `'words'.split()`. +- `ObjectVar` represents an expression that evaluates to an object. For example: `\{a: 2, b: 3}`, `\{deeply: \{nested: \{value: false}}}`. +- `NoneVar` represent null values. These can be either `undefined` or `null`. + +## Creating Vars + +State fields are converted to `Var` by default. Additionally, you can create a `Var` from Python values using `rx.Var.create()`: + +```py +rx.Var.create(4) # NumberVar +rx.Var.create("hello") # StringVar +rx.Var.create([1, 2, 3]) # ArrayVar +``` + +If you want to explicitly create a `Var` from a raw Javascript string, you can instantiate `rx.Var` directly: + +```py +rx.Var("2", _var_type=int).guess_type() # NumberVar +``` + +In the example above, `.guess_type()` will attempt to downcast from a generic `Var` type into `NumberVar`. +For this example, calling the function `.to(int)` can also be used in place of `.guess_type()`. + +## Operations + +The `Var` system also supports some other basic operations. +For example, `NumberVar` supports basic arithmetic operations like `+` and `-`, as in Python. +It also supports comparisons that return a `BooleanVar`. + +Custom `Var` operations can also be defined: + +```py +from reflex.vars import var_operation, var_operation_return, ArrayVar, NumberVar + +@var_operation +def multiply_array_values(a: ArrayVar): + return var_operation_return( + js_expression=f"\{a}.reduce((p, c) => p * c, 1)", + var_type=int, + ) + +def factorial(value: NumberVar): + return rx.cond( + value <= 1, + 1, + multiply_array_values(rx.Var.range(1, value+1)) + ) +``` + +Use `js_expression` to pass explicit JavaScript expressions; in the `multiply_array_values` example, we pass in a JavaScript expression that calculates the product of all elements in an array called `a` by using the reduce method to multiply each element with the accumulated result, starting from an initial value of 1. +Later, we leverage `rx.cond` in the' factorial' function, we instantiate an array using the `range` function, and pass this array to `multiply_array_values`. diff --git a/docs/api-routes/overview.md b/docs/api-routes/overview.md new file mode 100644 index 00000000000..30e5ef31f7a --- /dev/null +++ b/docs/api-routes/overview.md @@ -0,0 +1,152 @@ +```python exec +import reflex as rx +``` + +# API Transformer + +In addition to your frontend app, Reflex uses a FastAPI backend to serve your app. The API transformer feature allows you to transform or extend the ASGI app that serves your Reflex application. + +## Overview + +The API transformer provides a way to: + +1. Integrate existing FastAPI or Starlette applications with your Reflex app +2. Apply middleware or transformations to the ASGI app +3. Extend your Reflex app with additional API endpoints + +This is useful for creating a backend API that can be used for purposes beyond your Reflex app, or for integrating Reflex with existing backend services. + +## Using API Transformer + +You can set the `api_transformer` parameter when initializing your Reflex app: + +```python +import reflex as rx +from fastapi import FastAPI, Depends +from fastapi.security import OAuth2PasswordBearer + +# Create a FastAPI app +fastapi_app = FastAPI(title="My API") + +# Add routes to the FastAPI app +@fastapi_app.get("/api/items") +async def get_items(): + return dict(items=["Item1", "Item2", "Item3"]) + +# Create a Reflex app with the FastAPI app as the API transformer +app = rx.App(api_transformer=fastapi_app) +``` + +## Types of API Transformers + +The `api_transformer` parameter can accept: + +1. A Starlette or FastAPI instance +2. A callable that takes an ASGIApp and returns an ASGIApp +3. A sequence of the above + +### Using a FastAPI or Starlette Instance + +When you provide a FastAPI or Starlette instance as the API transformer, Reflex will mount its internal API to your app, allowing you to define additional routes: + +```python +import reflex as rx +from fastapi import FastAPI, Depends +from fastapi.security import OAuth2PasswordBearer + +# Create a FastAPI app with authentication +fastapi_app = FastAPI(title="Secure API") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# Add a protected route +@fastapi_app.get("/api/protected") +async def protected_route(token: str = Depends(oauth2_scheme)): + return dict(message="This is a protected endpoint") + +# Create a token endpoint +@fastapi_app.post("/token") +async def login(username: str, password: str): + # In a real app, you would validate credentials + if username == "user" and password == "password": + return dict(access_token="example_token", token_type="bearer") + return dict(error="Invalid credentials") + +# Create a Reflex app with the FastAPI app as the API transformer +app = rx.App(api_transformer=fastapi_app) +``` + +### Using a Callable Transformer + +You can also provide a callable that transforms the ASGI app: + +```python +import reflex as rx +from starlette.middleware.cors import CORSMiddleware + +# Create a transformer function that returns a transformed ASGI app +def add_cors_middleware(app): + # Wrap the app with CORS middleware and return the wrapped app + return CORSMiddleware( + app=app, + allow_origins=["https://example.com"], + allow_methods=["*"], + allow_headers=["*"], + ) + +# Create a Reflex app with the transformer +app = rx.App(api_transformer=add_cors_middleware) +``` + +### Using Multiple Transformers + +You can apply multiple transformers by providing a sequence: + +```python +import reflex as rx +from fastapi import FastAPI +from starlette.middleware import Middleware +from starlette.middleware.cors import CORSMiddleware + +# Create a FastAPI app +fastapi_app = FastAPI(title="My API") + +# Add routes to the FastAPI app +@fastapi_app.get("/api/items") +async def get_items(): + return dict(items=["Item1", "Item2", "Item3"]) + +# Create a transformer function +def add_logging_middleware(app): + # This is a simple example middleware that logs requests + async def middleware(scope, receive, send): + # Log the request path + path = scope["path"] + print("Request:", path) + await app(scope, receive, send) + return middleware + +# Create a Reflex app with multiple transformers +app = rx.App(api_transformer=[fastapi_app, add_logging_middleware]) +``` + +## Reserved Routes + +Some routes on the backend are reserved for the runtime of Reflex, and should not be overridden unless you know what you are doing. + +### Ping + +`localhost:8000/ping/`: You can use this route to check the health of the backend. + +The expected return is `"pong"`. + +### Event + +`localhost:8000/_event`: the frontend will use this route to notify the backend that an event occurred. + +```md alert error +# Overriding this route will break the event communication +``` + +### Upload + +`localhost:8000/_upload`: This route is used for the upload of file when using `rx.upload()`. diff --git a/docs/assets/overview.md b/docs/assets/overview.md new file mode 100644 index 00000000000..51ba7e1599d --- /dev/null +++ b/docs/assets/overview.md @@ -0,0 +1,95 @@ +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +``` + +# Assets + +Static files such as images and stylesheets can be placed in `assets/` folder of the project. These files can be referenced within your app. + +```md alert +# Assets are copied during the build process. + +Any files placed within the `assets/` folder at runtime will not be available to the app +when running in production mode. The `assets/` folder should only be used for static files. +``` + +## Referencing Assets + +There are two ways to reference assets in your Reflex app: + +### 1. Direct Path Reference + +To reference an image in the `assets/` folder, pass the relative path as a prop. + +For example, you can store your logo in your assets folder: + +```bash +assets +└── Reflex.svg +``` + +Then you can display it using a `rx.image` component: + +```python demo +rx.image(src=f"{REFLEX_ASSETS_CDN}other/Reflex.svg", width="5em") +``` + +```md alert +# Always prefix the asset path with a forward slash `/` to reference the asset from the root of the project, or it may not display correctly on non-root pages. +``` + +### 2. Using rx.asset Function + +The `rx.asset` function provides a more flexible way to reference assets in your app. It supports both local assets (in the app's `assets/` directory) and shared assets (placed next to your Python files). + +#### Local Assets + +Local assets are stored in the app's `assets/` directory and are referenced using `rx.asset`: + +```python demo +rx.image(src=rx.asset("Reflex.svg"), width="5em") +``` + +#### Shared Assets + +Shared assets are placed next to your Python file and are linked to the app's external assets directory. This is useful for creating reusable components with their own assets: + +```python box +# my_component.py +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN + +# my_script.js is located in the same directory as this Python file +def my_component(): + return rx.box( + rx.script(src=rx.asset("my_script.js", shared=True)), + "Component with custom script" + ) +``` + +You can also specify a subfolder for shared assets: + +```python box +# my_component.py +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN + +# image.png is located in a subfolder next to this Python file +def my_component_with_image(): + return rx.image( + src=rx.asset("image.png", shared=True, subfolder="images") + ) +``` + +```md alert +# Shared assets are linked to your app via symlinks. + +When using `shared=True`, the asset is symlinked from its original location to your app's external assets directory. This allows you to keep assets alongside their related code. +``` + +## Favicon + +The favicon is the small icon that appears in the browser tab. + +You can add a `favicon.ico` file to the `assets/` folder to change the favicon. diff --git a/docs/assets/upload_and_download_files.md b/docs/assets/upload_and_download_files.md new file mode 100644 index 00000000000..646c48ff45f --- /dev/null +++ b/docs/assets/upload_and_download_files.md @@ -0,0 +1,152 @@ +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +from pcweb.pages.docs import library +from pcweb.pages.docs import api_reference +from pcweb.styles.styles import get_code_style +from pcweb.styles.colors import c_color +``` + +# Files + +In addition to any assets you ship with your app, many web app will often need to receive or send files, whether you want to share media, allow user to import their data, or export some backend data. + +In this section, we will cover all you need to know for manipulating files in Reflex. + +## Assets vs Upload Directory + +Before diving into file uploads and downloads, it's important to understand the difference between assets and the upload directory in Reflex: + +```python eval +# Simple table comparing assets vs upload directory +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Feature"), + rx.table.column_header_cell("Assets"), + rx.table.column_header_cell("Upload Directory"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.cell(rx.text("Purpose", font_weight="bold")), + rx.table.cell(rx.text("Static files included with your app (images, stylesheets, scripts)")), + rx.table.cell(rx.text("Dynamic files uploaded by users during runtime")), + ), + rx.table.row( + rx.table.cell(rx.text("Location", font_weight="bold")), + rx.table.cell(rx.hstack( + rx.code("assets/", style=get_code_style("violet")), + rx.text(" folder or next to Python files (shared assets)"), + spacing="2", + )), + rx.table.cell(rx.hstack( + rx.code("uploaded_files/", style=get_code_style("violet")), + rx.text(" directory (configurable)"), + spacing="2", + )), + ), + rx.table.row( + rx.table.cell(rx.text("Access Method", font_weight="bold")), + rx.table.cell(rx.hstack( + rx.code("rx.asset()", style=get_code_style("violet")), + rx.text(" or direct path reference"), + spacing="2", + )), + rx.table.cell(rx.code("rx.get_upload_url()", style=get_code_style("violet"))), + ), + rx.table.row( + rx.table.cell(rx.text("When to Use", font_weight="bold")), + rx.table.cell(rx.text("For files that are part of your application's codebase")), + rx.table.cell(rx.text("For files that users upload or generate through your application")), + ), + rx.table.row( + rx.table.cell(rx.text("Availability", font_weight="bold")), + rx.table.cell(rx.text("Available at compile time")), + rx.table.cell(rx.text("Available at runtime")), + ), + ), + width="100%", +) +``` + +For more information about assets, see the [Assets Overview](/docs/assets/overview/). + +## Download + +If you want to let the users of your app download files from your server to their computer, Reflex offer you two way. + +### With a regular link + +For some basic usage, simply providing the path to your resource in a `rx.link` will work, and clicking the link will download or display the resource. + +```python demo +rx.link("Download", href="/reflex_banner.webp") +``` + +### With `rx.download` event + +Using the `rx.download` event will always prompt the browser to download the file, even if it could be displayed in the browser. + +The `rx.download` event also allows the download to be triggered from another backend event handler. + +```python demo +rx.button( + "Download", + on_click=rx.download(url="/reflex_banner.webp"), +) +``` + +`rx.download` lets you specify a name for the file that will be downloaded, if you want it to be different from the name on the server side. + +```python demo +rx.button( + "Download and Rename", + on_click=rx.download( + url="/reflex_banner.webp", + filename="different_name_logo.png" + ), +) +``` + +If the data to download is not already available at a known URL, pass the `data` directly to the `rx.download` event from the backend. + +```python demo exec +import random + +class DownloadState(rx.State): + @rx.event + def download_random_data(self): + return rx.download( + data=",".join([str(random.randint(0, 100)) for _ in range(10)]), + filename="random_numbers.csv" + ) + +def download_random_data_button(): + return rx.button( + "Download random numbers", + on_click=DownloadState.download_random_data + ) +``` + +The `data` arg accepts `str` or `bytes` data, a `data:` URI, `PIL.Image`, or any state Var. If the Var is not already a string, it will be converted to a string using `JSON.stringify`. This allows complex state structures to be offered as JSON downloads. + +Reference page for `rx.download` [here]({api_reference.special_events.path}#rx.download). + +## Upload + +Uploading files to your server let your users interact with your app in a different way than just filling forms to provide data. + +The component `rx.upload` let your users upload files on the server. + +Here is a basic example of how it is used: + +```python +def index(): + return rx.fragment( + rx.upload(rx.text("Upload files"), rx.icon(tag="upload")), + rx.button(on_submit=State.) + ) +``` + +For detailed information, see the reference page of the component [here]({library.forms.upload.path}). diff --git a/docs/authentication/authentication_overview.md b/docs/authentication/authentication_overview.md new file mode 100644 index 00000000000..72dc4de4ecd --- /dev/null +++ b/docs/authentication/authentication_overview.md @@ -0,0 +1,28 @@ +```python exec +from pcweb.pages.docs import vars +``` + +# Authentication Overview + +Many apps require authentication to manage users. There are a few different ways to accomplish this in Reflex: + +We have solutions that currently exist outside of the core framework: + +1. Local Auth: Uses your own database: https://github.com/masenf/reflex-local-auth +2. Google Auth: Uses sign in with Google: https://github.com/masenf/reflex-google-auth +3. Captcha: Generates tests that humans can pass but automated systems cannot: https://github.com/masenf/reflex-google-recaptcha-v2 +4. Magic Link Auth: A passwordless login method that sends a unique, one-time-use URL to a user's email: https://github.com/masenf/reflex-magic-link-auth +5. Clerk Auth: A community member wrapped this component and hooked it up in this app: https://github.com/TimChild/reflex-clerk-api +6. Descope Auth: Enables authentication with Descope, supporting passwordless, social login, SSO, and MFA: https://github.com/descope-sample-apps/reflex-descope-auth + +If you're using the AI Builder, you can also use the built-in [Authentication Integrations](/docs/ai-builder/integrations/overview) which include Azure Auth, Google Auth, Okta Auth, and Descope. + +## Guidance for Implementing Authentication + +- Store sensitive user tokens and information in [backend-only vars]({vars.base_vars.path}#backend-only-vars). +- Validate user session and permissions for each event handler that performs an authenticated action and all computed vars or loader events that access private data. +- All content that is statically rendered in the frontend (for example, data hardcoded or loaded at compile time in the UI) will be publicly available, even if the page redirects to a login or uses `rx.cond` to hide content. +- Only data that originates from state can be truly private and protected. +- When using cookies or local storage, a signed JWT can detect and invalidate any local tampering. + +More auth documentation on the way. Check back soon! diff --git a/docs/client_storage/overview.md b/docs/client_storage/overview.md new file mode 100644 index 00000000000..7016080a1d9 --- /dev/null +++ b/docs/client_storage/overview.md @@ -0,0 +1,45 @@ +```python exec +import reflex as rx +``` + +# Client-storage + +You can use the browser's local storage to persist state between sessions. +This allows user preferences, authentication cookies, other bits of information +to be stored on the client and accessed from different browser tabs. + +A client-side storage var looks and acts like a normal `str` var, except the +default value is either `rx.Cookie` or `rx.LocalStorage` depending on where the +value should be stored. The key name will be based on the var name, but this +can be overridden by passing `name="my_custom_name"` as a keyword argument. + +For more information see [Browser Storage](/docs/api-reference/browser-storage/). + +Try entering some values in the text boxes below and then load the page in a separate +tab or check the storage section of browser devtools to see the values saved in the browser. + +```python demo exec +class ClientStorageState(rx.State): + my_cookie: str = rx.Cookie("") + my_local_storage: str = rx.LocalStorage("") + custom_cookie: str = rx.Cookie(name="CustomNamedCookie", max_age=3600) + + @rx.event + def set_my_cookie(self, value: str): + self.my_cookie = value + + @rx.event + def set_my_local_storage(self, value: str): + self.my_local_storage = value + + @rx.event + def set_custom_cookie(self, value: str): + self.custom_cookie = value + +def client_storage_example(): + return rx.vstack( + rx.hstack(rx.text("my_cookie"), rx.input(value=ClientStorageState.my_cookie, on_change=ClientStorageState.set_my_cookie)), + rx.hstack(rx.text("my_local_storage"), rx.input(value=ClientStorageState.my_local_storage, on_change=ClientStorageState.set_my_local_storage)), + rx.hstack(rx.text("custom_cookie"), rx.input(value=ClientStorageState.custom_cookie, on_change=ClientStorageState.set_custom_cookie)), + ) +``` diff --git a/docs/components/conditional_rendering.md b/docs/components/conditional_rendering.md new file mode 100644 index 00000000000..9def2313da3 --- /dev/null +++ b/docs/components/conditional_rendering.md @@ -0,0 +1,129 @@ +```python exec +import reflex as rx + +from pcweb.pages.docs import library +from pcweb.pages import docs +``` + +# Conditional Rendering + +Recall from the [basics]({docs.getting_started.basics.path}) that we cannot use Python `if/else` statements when referencing state vars in Reflex. Instead, use the `rx.cond` component to conditionally render components or set props based on the value of a state var. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=6040&end=6463 +# Video: Conditional Rendering +``` + +```md alert +# Check out the API reference for [cond docs]({library.dynamic_rendering.cond.path}). +``` + +```python eval +rx.box(height="2em") +``` + +Below is a simple example showing how to toggle between two text components by checking the value of the state var `show`. + +```python demo exec +class CondSimpleState(rx.State): + show: bool = True + + @rx.event + def change(self): + self.show = not (self.show) + + +def cond_simple_example(): + return rx.vstack( + rx.button("Toggle", on_click=CondSimpleState.change), + rx.cond( + CondSimpleState.show, + rx.text("Text 1", color="blue"), + rx.text("Text 2", color="red"), + ), + ) +``` + +If `show` is `True` then the first component is rendered (in this case the blue text). Otherwise the second component is rendered (in this case the red text). + +## Conditional Props + +You can also set props conditionally using `rx.cond`. In this example, we set the `color` prop of a text component based on the value of the state var `show`. + +```python demo exec +class PropCondState(rx.State): + + value: int + + @rx.event + def set_end(self, value: list[int | float]): + self.value = value[0] + + +def cond_prop(): + return rx.slider( + default_value=[50], + on_value_commit=PropCondState.set_end, + color_scheme=rx.cond(PropCondState.value > 50, "green", "pink"), + width="100%", + ) +``` + +## Var Operations + +You can use [var operations]({docs.vars.var_operations.path}) with the `cond` component for more complex conditions. See the full [cond reference]({library.dynamic_rendering.cond.path}) for more details. + +## Multiple Conditional Statements + +The [`rx.match`]({library.dynamic_rendering.match.path}) component in Reflex provides a powerful alternative to`rx.cond` for handling multiple conditional statements and structural pattern matching. This component allows you to handle multiple conditions and their associated components in a cleaner and more readable way compared to nested `rx.cond` structures. + +```python demo exec +from typing import List + +import reflex as rx + + +class MatchState(rx.State): + cat_breed: str = "" + animal_options: List[str] = [ + "persian", + "siamese", + "maine coon", + "ragdoll", + "pug", + "corgi", + ] + + @rx.event + def set_cat_breed(self, breed: str): + self.cat_breed = breed + + +def match_demo(): + return rx.flex( + rx.match( + MatchState.cat_breed, + ("persian", rx.text("Persian cat selected.")), + ("siamese", rx.text("Siamese cat selected.")), + ( + "maine coon", + rx.text("Maine Coon cat selected."), + ), + ("ragdoll", rx.text("Ragdoll cat selected.")), + rx.text("Unknown cat breed selected."), + ), + rx.select( + [ + "persian", + "siamese", + "maine coon", + "ragdoll", + "pug", + "corgi", + ], + value=MatchState.cat_breed, + on_change=MatchState.set_cat_breed, + ), + direction="column", + gap="2", + ) +``` diff --git a/docs/components/html_to_reflex.md b/docs/components/html_to_reflex.md new file mode 100644 index 00000000000..42dcd0f5f60 --- /dev/null +++ b/docs/components/html_to_reflex.md @@ -0,0 +1,16 @@ +# Convert HTML to Reflex + +To convert HTML, CSS, or any design into Reflex code, use our AI-powered build tool at [Reflex Build](https://build.reflex.dev). + +Simply paste your HTML, CSS, or describe what you want to build, and our AI will generate the corresponding Reflex code for you. + +## How to use Reflex Build + +1. Go to [Reflex Build](https://build.reflex.dev) +2. Paste your HTML/CSS code or describe your design +3. The AI will automatically generate Reflex code +4. Copy the generated code into your Reflex application + +## Convert Figma file to Reflex + +Check out this [Notion doc](https://www.notion.so/reflex-dev/Convert-HTML-to-Reflex-fe22d0641dcd4d5c91c8404ca41c7e77) for a walk through on how to convert a Figma file into Reflex code. diff --git a/docs/components/props.md b/docs/components/props.md new file mode 100644 index 00000000000..9c53a92cfe9 --- /dev/null +++ b/docs/components/props.md @@ -0,0 +1,95 @@ +```python exec +import reflex as rx +from pcweb.pages.docs.library import library +from pcweb.pages import docs +``` + +# Props + +Props modify the behavior and appearance of a component. They are passed in as keyword arguments to a component. + +## Component Props + +There are props that are shared between all components, but each component can also define its own props. + +For example, the `rx.image` component has a `src` prop that specifies the URL of the image to display and an `alt` prop that specifies the alternate text for the image. + +```python demo +rx.image( + src="https://web.reflex-assets.dev/other/logo.jpg", + alt="Reflex Logo", +) +``` + +Check the docs for the component you are using to see what props are available and how they affect the component (see the `rx.image` [reference]({docs.library.media.image.path}#api-reference) page for example). + +## Common Props + +Components support many standard HTML properties as props. For example: the HTML [id]({"https://www.w3schools.com/html/html_id.asp"}) property is exposed directly as the prop `id`. The HTML [className]({"https://www.w3schools.com/jsref/prop_html_classname.asp"}) property is exposed as the prop `class_name` (note the Pythonic snake_casing!). + +```python demo +rx.box( + "Hello World", + id="box-id", + class_name=["class-name-1", "class-name-2",], +) +``` + +In the example above, the `class_name` prop of the `rx.box` component is assigned a list of class names. This means the `rx.box` component will be styled with the CSS classes `class-name-1` and `class-name-2`. + +## Style Props + +In addition to component-specific props, most built-in components support a full range of style props. You can use any [CSS property](https://www.w3schools.com/cssref/index.php) to style a component. + +```python demo +rx.button( + "Fancy Button", + border_radius="1em", + box_shadow="rgba(151, 65, 252, 0.8) 0 15px 30px -10px", + background_image="linear-gradient(144deg,#AF40FF,#5B42F3 50%,#00DDEB)", + box_sizing="border-box", + color="white", + opacity= 1, +) +``` + +See the [styling docs]({docs.styling.overview.path}) to learn more about customizing the appearance of your app. + +## Binding Props to State + +```md alert warning +# Optional: Learn all about [State]({docs.state.overview.path}) first. +``` + +Reflex apps define [State]({docs.state.overview.path}) classes that hold variables that can change over time. + +State may be modified in response to things like user input like clicking a button, or in response to events like loading a page. + +State vars can be bound to component props, so that the UI always reflects the current state of the app. + +Try clicking the badge below to change its color. + +```python demo exec +class PropExampleState(rx.State): + text: str = "Hello World" + color: str = "red" + + @rx.event + def flip_color(self): + if self.color == "red": + self.color = "blue" + else: + self.color = "red" + + +def index(): + return rx.button( + PropExampleState.text, + color_scheme=PropExampleState.color, + on_click=PropExampleState.flip_color, + ) +``` + +In this example, the `color_scheme` prop is bound to the `color` state var. + +When the `flip_color` event handler is called, the `color` var is updated, and the `color_scheme` prop is updated to match. diff --git a/docs/components/rendering_iterables.md b/docs/components/rendering_iterables.md new file mode 100644 index 00000000000..0c434cb8a54 --- /dev/null +++ b/docs/components/rendering_iterables.md @@ -0,0 +1,302 @@ +```python exec +import reflex as rx + +from pcweb.pages import docs +``` + +# Rendering Iterables + +Recall again from the [basics]({docs.getting_started.basics.path}) that we cannot use Python `for` loops when referencing state vars in Reflex. Instead, use the `rx.foreach` component to render components from a collection of data. + +For dynamic content that should automatically scroll to show the newest items, consider using the [auto scroll]({docs.library.dynamic_rendering.auto_scroll.path}) component together with `rx.foreach`. + +```python demo exec +class IterState(rx.State): + color: list[str] = [ + "red", + "green", + "blue", + ] + + +def colored_box(color: str): + return rx.button(color, background_color=color) + + +def dynamic_buttons(): + return rx.vstack( + rx.foreach(IterState.color, colored_box), + ) + +``` + +Here's the same example using a lambda function. + +```python +def dynamic_buttons(): + return rx.vstack( + rx.foreach(IterState.color, lambda color: colored_box(color)), + ) +``` + +You can also use lambda functions directly with components without defining a separate function. + +```python +def dynamic_buttons(): + return rx.vstack( + rx.foreach(IterState.color, lambda color: rx.button(color, background_color=color)), + ) +``` + +In this first simple example we iterate through a `list` of colors and render a dynamic number of buttons. + +The first argument of the `rx.foreach` function is the state var that you want to iterate through. The second argument is a function that takes in an item from the data and returns a component. In this case, the `colored_box` function takes in a color and returns a button with that color. + +## For vs Foreach + +```md definition +# Regular For Loop + +- Use when iterating over constants. + +# Foreach + +- Use when iterating over state vars. +``` + +The above example could have been written using a regular Python `for` loop, since the data is constant. + +```python demo exec +colors = ["red", "green", "blue"] +def dynamic_buttons_for(): + return rx.vstack( + [colored_box(color) for color in colors], + ) +``` + +However, as soon as you need the data to be dynamic, you must use `rx.foreach`. + +```python demo exec +class DynamicIterState(rx.State): + color: list[str] = [ + "red", + "green", + "blue", + ] + + def add_color(self, form_data): + self.color.append(form_data["color"]) + +def dynamic_buttons_foreach(): + return rx.vstack( + rx.foreach(DynamicIterState.color, colored_box), + rx.form( + rx.input(name="color", placeholder="Add a color"), + rx.button("Add"), + on_submit=DynamicIterState.add_color, + ) + ) +``` + +## Render Function + +The function to render each item can be defined either as a separate function or as a lambda function. In the example below, we define the function `colored_box` separately and pass it to the `rx.foreach` function. + +```python demo exec +class IterState2(rx.State): + color: list[str] = [ + "red", + "green", + "blue", + ] + +def colored_box(color: rx.Var[str]): + return rx.button(color, background_color=color) + +def dynamic_buttons2(): + return rx.vstack( + rx.foreach(IterState2.color, colored_box), + ) + +``` + +Notice that the type annotation for the `color` parameter in the `colored_box` function is `rx.Var[str]` (rather than just `str`). This is because the `rx.foreach` function passes the item as a `Var` object, which is a wrapper around the actual value. This is what allows us to compile the frontend without knowing the actual value of the state var (which is only known at runtime). + +## Enumerating Iterables + +The function can also take an index as a second argument, meaning that we can enumerate through data as shown in the example below. + +```python demo exec +class IterIndexState(rx.State): + color: list[str] = [ + "red", + "green", + "blue", + ] + + +def create_button(color: rx.Var[str], index: int): + return rx.box( + rx.button(f"{index + 1}. {color}"), + padding_y="0.5em", + ) + +def enumerate_foreach(): + return rx.vstack( + rx.foreach( + IterIndexState.color, + create_button + ), + ) +``` + +Here's the same example using a lambda function. + +```python +def enumerate_foreach(): + return rx.vstack( + rx.foreach( + IterIndexState.color, + lambda color, index: create_button(color, index) + ), + ) +``` + +## Iterating Dictionaries + +We can iterate through a `dict` using a `foreach`. When the dict is passed through to the function that renders each item, it is presented as a list of key-value pairs `[("sky", "blue"), ("balloon", "red"), ("grass", "green")]`. + +```python demo exec +class SimpleDictIterState(rx.State): + color_chart: dict[str, str] = { + "sky": "blue", + "balloon": "red", + "grass": "green", + } + + +def display_color(color: list): + # color is presented as a list key-value pairs [("sky", "blue"), ("balloon", "red"), ("grass", "green")] + return rx.box(rx.text(color[0]), bg=color[1], padding_x="1.5em") + + +def dict_foreach(): + return rx.grid( + rx.foreach( + SimpleDictIterState.color_chart, + display_color, + ), + columns="3", + ) + +``` + +```md alert warning +# Dict Type Annotation. + +It is essential to provide the correct full type annotation for the dictionary in the state definition (e.g., `dict[str, str]` instead of `dict`) to ensure `rx.foreach` works as expected. Proper typing allows Reflex to infer and validate the structure of the data during rendering. +``` + +## Nested examples + +`rx.foreach` can be used with nested state vars. Here we use nested `foreach` components to render the nested state vars. The `rx.foreach(project["technologies"], get_badge)` inside of the `project_item` function, renders the `dict` values which are of type `list`. The `rx.box(rx.foreach(NestedStateFE.projects, project_item))` inside of the `projects_example` function renders each `dict` inside of the overall state var `projects`. + +```python demo exec +class NestedStateFE(rx.State): + projects: list[dict[str, list]] = [ + { + "technologies": ["Next.js", "Prisma", "Tailwind", "Google Cloud", "Docker", "MySQL"] + }, + { + "technologies": ["Python", "Flask", "Google Cloud", "Docker"] + } + ] + +def get_badge(technology: rx.Var[str]) -> rx.Component: + return rx.badge(technology, variant="soft", color_scheme="green") + +def project_item(project: rx.Var[dict[str, list]]) -> rx.Component: + return rx.box( + rx.hstack( + rx.foreach(project["technologies"], get_badge) + ), + ) + +def projects_example() -> rx.Component: + return rx.box(rx.foreach(NestedStateFE.projects, project_item)) +``` + +If you want an example where not all of the values in the dict are the same type then check out the example on [var operations using foreach]({docs.vars.var_operations.path}). + +Here is a further example of how to use `foreach` with a nested data structure. + +```python demo exec +class NestedDictIterState(rx.State): + color_chart: dict[str, list[str]] = { + "purple": ["red", "blue"], + "orange": ["yellow", "red"], + "green": ["blue", "yellow"], + } + + +def display_colors(color: rx.Var[tuple[str, list[str]]]): + return rx.vstack( + rx.text(color[0], color=color[0]), + rx.hstack( + rx.foreach( + color[1], + lambda x: rx.box( + rx.text(x, color="black"), bg=x + ), + ) + ), + ) + + +def nested_dict_foreach(): + return rx.grid( + rx.foreach( + NestedDictIterState.color_chart, + display_colors, + ), + columns="3", + ) + +``` + +## Foreach with Cond + +We can also use `foreach` with the `cond` component. + +In this example we define the function `render_item`. This function takes in an `item`, uses the `cond` to check if the item `is_packed`. If it is packed it returns the `item_name` with a `✔` next to it, and if not then it just returns the `item_name`. We use the `foreach` to iterate over all of the items in the `to_do_list` using the `render_item` function. + +```python demo exec +import dataclasses + +@dataclasses.dataclass +class ToDoListItem: + item_name: str + is_packed: bool + +class ForeachCondState(rx.State): + to_do_list: list[ToDoListItem] = [ + ToDoListItem(item_name="Space suit", is_packed=True), + ToDoListItem(item_name="Helmet", is_packed=True), + ToDoListItem(item_name="Back Pack", is_packed=False), + ] + + +def render_item(item: rx.Var[ToDoListItem]): + return rx.cond( + item.is_packed, + rx.list.item(item.item_name + ' ✔'), + rx.list.item(item.item_name), + ) + +def packing_list(): + return rx.vstack( + rx.text("Sammy's Packing List"), + rx.list(rx.foreach(ForeachCondState.to_do_list, render_item)), + ) + +``` diff --git a/docs/custom-components/command-reference.md b/docs/custom-components/command-reference.md new file mode 100644 index 00000000000..56b95b74676 --- /dev/null +++ b/docs/custom-components/command-reference.md @@ -0,0 +1,157 @@ +```python exec +from pcweb.pages import docs +``` + +# Command Reference + +The custom component commands are under `reflex component` subcommand. To see the list of available commands, run `reflex component --help`. To see the manual on a specific command, run `reflex component --help`, for example, `reflex component init --help`. + +```bash +reflex component --help +``` + +```text +Usage: reflex component [OPTIONS] COMMAND [ARGS]... + + Subcommands for creating and publishing Custom Components. + +Options: + --help Show this message and exit. + +Commands: + init Initialize a custom component. + build Build a custom component. + share Collect more details on the published package for gallery. +``` + +## reflex component init + +Below is an example of running the `init` command. + +```bash +reflex component init +``` + +```text +reflex component init +─────────────────────────────────────── Initializing reflex-google-auth project ─────────────────────────────────────── +Info: Populating pyproject.toml with package name: reflex-google-auth +Info: Initializing the component directory: custom_components/reflex_google_auth +Info: Creating app for testing: google_auth_demo +──────────────────────────────────────────── Initializing google_auth_demo ──────────────────────────────────────────── +[07:58:16] Initializing the app directory. console.py:85 + Initializing the web directory. console.py:85 +Success: Initialized google_auth_demo +─────────────────────────────────── Installing reflex-google-auth in editable mode. ─────────────────────────────────── +Info: Package reflex-google-auth installed! +Custom component initialized successfully! +─────────────────────────────────────────────────── Project Summary ─────────────────────────────────────────────────── +[ README.md ]: Package description. Please add usage examples. +[ pyproject.toml ]: Project configuration file. Please fill in details such as your name, email, homepage URL. +[ custom_components/ ]: Custom component code template. Start by editing it with your component implementation. +[ google_auth_demo/ ]: Demo App. Add more code to this app and test. +``` + +The `init` command uses the current enclosing folder name to construct a python package name, typically in the kebab case. For example, if running init in folder `google_auth`, the package name will be `reflex-google-auth`. The added prefix reduces the chance of name collision on PyPI (the Python Package Index), and it indicates that the package is a Reflex custom component. The user can override the package name by providing the `--package-name` option. + +The `init` command creates a set of files and folders prefilled with the package name and other details. During the init, the `custom_component` folder is installed locally in editable mode, so a developer can incrementally develop and test with ease. The changes in component implementation is automatically reflected where it is used. Below is the folder structure after the `init` command. + +```text +google_auth/ +├── pyproject.toml +├── README.md +├── custom_components/ +│ └── reflex_google_auth/ +│ ├── google_auth.py +│ └── __init__.py +└── google_auth_demo/ + └── assets/ + google_auth_demo/ + requirements.txt + rxconfig.py +``` + +### pyproject.toml + +The `pyproject.toml` is required for the package to build and be published. It is prefilled with information such as the package name, version (`0.0.1`), author name and email, homepage URL. By default the **Apache-2.0** license is used, the same as Reflex. If any of this information requires update, the user can edit the file by hand. + +### README + +The `README.md` file is created with installation instructions, e.g. `pip install reflex-google-auth`, and a brief description of the package. Typically the `README.md` contains usage examples. On PyPI, the `README.md` is rendered as part of the package page. + +### Custom Components Folder + +The `custom_components` folder is where the actual implementation is. Do not worry about this folder name: there is no need to change it. It is where `pyproject.toml` specifies the source of the python package is. The published package contains the contents inside it, excluding this folder. + +`reflex_google_auth` is the top folder for importable code. The `reflex_google_auth/__init__.py` imports everything from the `reflex_google_auth/google_auth.py`. For the user of the package, the import looks like `from reflex_google_auth import ABC, XYZ`. + +`reflex_google_auth/google_auth.py` is prefilled with code example and instructions from the [wrapping react guide]({docs.wrapping_react.overview.path}). + +### Demo App Folder + +A demo app is generated inside `google_auth_demo` folder with import statements and example usage of the component. This is a regular Reflex app. Go into this directory and start using any reflex commands for testing. + +### Help Manual + +The help manual is shown when adding the `--help` option to the command. + +```bash +reflex component init --help +``` + +```text +Usage: reflex component init [OPTIONS] + + Initialize a custom component. + + Args: library_name: The name of the library. install: Whether to + install package from this local custom component in editable mode. + loglevel: The log level to use. + + Raises: Exit: If the pyproject.toml already exists. + +Options: + --library-name TEXT The name of your library. On PyPI, package + will be published as `reflex-{library- + name}`. + --install / --no-install Whether to install package from this local + custom component in editable mode. + [default: install] + --loglevel [debug|info|warning|error|critical] + The log level to use. [default: + LogLevel.INFO] + --help Show this message and exit. +``` + +## reflex component publish + +To publish to a package index, a user is required to already have an account with them. As of **0.7.5**, Reflex does not handle the publishing process for you. You can do so manually by first running `reflex component build` followed by `twine upload` or `uv publish` or your choice of a publishing utility. + +You can then share your build on our website with `reflex component share`. + +## reflex component build + +It is not required to run the `build` command separately before publishing. The `publish` command will build the package if it is not already built. The `build` command is provided for the user's convenience. + +The `build` command generates the `.tar.gz` and `.whl` distribution files to be uploaded to the desired package index, for example, PyPI. This command must be run at the top level of the project where the `pyproject.toml` file is. As a result of a successful build, there is a new `dist` folder with the distribution files. + +```bash +reflex component build --help +``` + +```text +Usage: reflex component build [OPTIONS] + + Build a custom component. Must be run from the project root directory where + the pyproject.toml is. + + Args: loglevel: The log level to use. + + Raises: Exit: If the build fails. + +Options: + --loglevel [debug|info|warning|error|critical] + The log level to use. [default: + LogLevel.INFO] + --help Show this message and exit. +``` diff --git a/docs/custom-components/overview.md b/docs/custom-components/overview.md new file mode 100644 index 00000000000..3970c73d97f --- /dev/null +++ b/docs/custom-components/overview.md @@ -0,0 +1,75 @@ +# Custom Components Overview + +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +from pcweb.pages.docs import custom_components +from pcweb.pages.docs.custom_components import custom_components as custom_components_gallery +``` + +Reflex users create many components of their own: ready to use high level components, or nicely wrapped React components. With **Custom Components**, the community can easily share these components now. + +Release **0.4.3** introduces a series of `reflex component` commands that help developers wrap react components, test, and publish them as python packages. As shown in the image below, there are already a few custom components published on PyPI, such as `reflex-spline`, `reflex-webcam`. + +Check out the custom components gallery [here]({custom_components_gallery.path}). + +```python eval +rx.center( + rx.image(src=f"{REFLEX_ASSETS_CDN}custom_components/pypi_reflex_custom_components.webp", width="400px", border_radius="15px", border="1px solid"), +) +``` + +## Prerequisites for Publishing + +In order to publish a Python package, an account is required with a python package index, for example, PyPI. The documentation to create accounts and generate API tokens can be found on their websites. For a quick reference, check out our [Prerequisites for Publishing]({custom_components.prerequisites_for_publishing.path}) page. + +## Steps to Publishing + +Follow these steps to publish the custom component as a python package: + +1. `reflex component init`: creates a new custom component project from templates. +2. dev and test: developer implements and tests the custom component. +3. `reflex component build`: builds the package. +4. `twine upload` or `uv publish`: uploads the package to a python package index. + +### Initialization + +```bash +reflex component init +``` + +First create a new folder for your custom component project, for example `color_picker`. The package name will be `reflex-color-picker`. The prefix `reflex-` is intentionally added for all custom components for easy search on PyPI. If you prefer a particular name for the package, you can either change it manually in the `pyproject.toml` file or add the `--library-name` option in the `reflex component init` command initially. + +Run `reflex component init`, and a set of files and folders will be created in the `color_picker` folder. The `pyproject.toml` file is the configuration file for the project. The `custom_components` folder is where the custom component implementation is. The `color_picker_demo` folder is a demo Reflex app that uses the custom component. If this is the first time of creating python packages, it is encouraged to browse through all the files (there are not that many) to understand the structure of the project. + +```bash +color_picker/ +├── pyproject.toml <- Configuration file +├── README.md +├── .gitignore <- Exclude dist/ and metadata folders +├── custom_components/ +│ └── reflex_color_picker/ <- Custom component source directory +│ ├── color_picker.py +│ └── __init__.py +└── color_picker_demo/ <- Demo Reflex app directory + └── assets/ + color_picker_demo/ + requirements.txt + rxconfig.py +``` + +### Develop and Test + +After finishing the custom component implementation, the user is encouraged to fully test it before publishing. The generated Reflex demo app `color_picker_demo` is a good place to start. It is a regular Reflex app prefilled with imports and usage of this component. During the init, the `custom_component` folder is installed locally in editable mode, so a developer can incrementally develop and test with ease. The changes in component implementation are automatically reflected in the demo app. + +### Publish + +```bash +reflex component build +``` + +Once you're ready to publish your package, run `reflex component build` to build the package. The command builds the distribution files if they are not already built. The end result is a `dist` folder containing the distribution files. The user does not need to do anything manually with these distribution files. + +In order to publish these files as a Python package, you need to use a publishing utility. Any would work, but we recommend either [Twine](https://twine.readthedocs.io/en/stable/) or (uv)[https://docs.astral.sh/uv/guides/package/#publishing-your-package]. Make sure to keep your package version in pyproject.toml updated. + +You can also share your components with the rest of the community at our website using the command `reflex component share`. See you there! diff --git a/docs/custom-components/prerequisites-for-publishing.md b/docs/custom-components/prerequisites-for-publishing.md new file mode 100644 index 00000000000..f836e4dad12 --- /dev/null +++ b/docs/custom-components/prerequisites-for-publishing.md @@ -0,0 +1,40 @@ +# Python Package Index + +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +from pcweb.styles.colors import c_color +image_style = { + "width": "400px", + "border_radius": "12px", + "border": f"1px solid {c_color('slate', 5)}", +} +``` + +In order to publish a Python package, you need to use a publishing utility. Any would work, but we recommend either [Twine](https://twine.readthedocs.io/en/stable/) or [uv](https://docs.astral.sh/uv/guides/package/#publishing-your-package). + +## PyPI + +It is straightforward to create accounts and API tokens with PyPI. There is official help on the [PyPI website](https://pypi.org/help/). For a quick reference here, go to the top right corner of the PyPI website and look for the button to register and fill out personal information. + +```python eval +rx.center( + rx.image(src=f"{REFLEX_ASSETS_CDN}custom_components/pypi_register.webp", style=image_style, margin_bottom="16px", loading="lazy"), +) +``` + +A user can use username and password to authenticate with PyPI when publishing. + +```python eval +rx.center( + rx.image(src=f"{REFLEX_ASSETS_CDN}custom_components/pypi_account_settings.webp", style=image_style, margin_bottom="16px", loading="lazy"), +) +``` + +Scroll down to the API tokens section and click on the "Add API token" button. Fill out the form and click "Generate API token". + +```python eval +rx.center( + rx.image(src=f"{REFLEX_ASSETS_CDN}custom_components/pypi_api_tokens.webp", style=image_style, width="700px", loading="lazy"), +) +``` diff --git a/docs/database/overview.md b/docs/database/overview.md new file mode 100644 index 00000000000..830d24a6716 --- /dev/null +++ b/docs/database/overview.md @@ -0,0 +1,87 @@ +# Database + +Reflex uses [sqlmodel](https://sqlmodel.tiangolo.com) to provide a built-in ORM wrapping SQLAlchemy. + +The examples on this page refer specifically to how Reflex uses various tools to +expose an integrated database interface. Only basic use cases will be covered +below, but you can refer to the +[sqlmodel tutorial](https://sqlmodel.tiangolo.com/tutorial/select/) +for more examples and information, just replace `SQLModel` with `rx.Model` and +`Session(engine)` with `rx.session()` + +For advanced use cases, please see the +[SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/orm/quickstart.html) (v1.4). + +```md alert info +# Using NoSQL Databases + +If you are using a NoSQL database (e.g. MongoDB), you can work with it in Reflex by installing the appropriate Python client library. In this case, Reflex will not provide any ORM features. +``` + +## Connecting + +Reflex provides a built-in SQLite database for storing and retrieving data. + +You can connect to your own SQL compatible database by modifying the +`rxconfig.py` file with your database url. + +```python +config = rx.Config( + app_name="my_app", + db_url="sqlite:///reflex.db", +) +``` + +For more examples of database URLs that can be used, see the [SQLAlchemy +docs](https://docs.sqlalchemy.org/en/14/core/engines.html#backend-specific-urls). +Be sure to install the appropriate DBAPI driver for the database you intend to +use. + +## Tables + +To create a table make a class that inherits from `rx.Model` with and specify +that it is a table. + +```python +class User(rx.Model, table=True): + username: str + email: str + password: str +``` + +## Migrations + +Reflex leverages [alembic](https://alembic.sqlalchemy.org/en/latest/) +to manage database schema changes. + +Before the database feature can be used in a new app you must call `reflex db init` +to initialize alembic and create a migration script with the current schema. + +After making changes to the schema, use +`reflex db makemigrations --message 'something changed'` +to generate a script in the `alembic/versions` directory that will update the +database schema. It is recommended that generated scripts be inspected before applying them. + +Bear in mind that your newest models will not be detected by the `reflex db makemigrations` +command unless imported and used somewhere within the application. + +The `reflex db migrate` command is used to apply migration scripts to bring the +database up to date. During app startup, if Reflex detects that the current +database schema is not up to date, a warning will be displayed on the console. + +## Queries + +To query the database you can create a `rx.session()` +which handles opening and closing the database connection. + +You can use normal SQLAlchemy queries to query the database. + +```python +with rx.session() as session: + session.add(User(username="test", email="admin@reflex.dev", password="admin")) + session.commit() +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=6835&end=8225 +# Video: Tutorial of Database Model with Forms, Model Field Changes and Migrations, and adding a DateTime Field +``` diff --git a/docs/database/queries.md b/docs/database/queries.md new file mode 100644 index 00000000000..61267afb6d4 --- /dev/null +++ b/docs/database/queries.md @@ -0,0 +1,345 @@ +# Queries + +Queries are used to retrieve data from a database. + +A query is a request for information from a database table or combination of +tables. A query can be used to retrieve data from a single table or multiple +tables. A query can also be used to insert, update, or delete data from a table. + +## Session + +To execute a query you must first create a `rx.session`. You can use the session +to query the database using SQLModel or SQLAlchemy syntax. + +The `rx.session` statement will automatically close the session when the code +block is finished. **If `session.commit()` is not called, the changes will be +rolled back and not persisted to the database.** The code can also explicitly +rollback without closing the session via `session.rollback()`. + +The following example shows how to create a session and query the database. +First we create a table called `User`. + +```python +class User(rx.Model, table=True): + username: str + email: str +``` + +### Select + +Then we create a session and query the User table. + +```python +class QueryUser(rx.State): + name: str + users: list[User] + + @rx.event + def get_users(self): + with rx.session() as session: + self.users = session.exec( + User.select().where( + User.username.contains(self.name))).all() +``` + +The `get_users` method will query the database for all users that contain the +value of the state var `name`. + +### Insert + +Similarly, the `session.add()` method to add a new record to the +database or persist an existing object. + +```python +class AddUser(rx.State): + username: str + email: str + + @rx.event + def add_user(self): + with rx.session() as session: + session.add(User(username=self.username, email=self.email)) + session.commit() +``` + +### Update + +To update the user, first query the database for the object, make the desired +modifications, `.add` the object to the session and finally call `.commit()`. + +```python +class ChangeEmail(rx.State): + username: str + email: str + + @rx.event + def modify_user(self): + with rx.session() as session: + user = session.exec(User.select().where( + (User.username == self.username))).first() + user.email = self.email + session.add(user) + session.commit() +``` + +### Delete + +To delete a user, first query the database for the object, then call +`.delete()` on the session and finally call `.commit()`. + +```python +class RemoveUser(rx.State): + username: str + + @rx.event + def delete_user(self): + with rx.session() as session: + user = session.exec(User.select().where( + User.username == self.username)).first() + session.delete(user) + session.commit() +``` + +## ORM Object Lifecycle + +The objects returned by queries are bound to the session that created them, and cannot generally +be used outside that session. After adding or updating an object, not all fields are automatically +updated, so accessing certain attributes may trigger additional queries to refresh the object. + +To avoid this, the `session.refresh()` method can be used to update the object explicitly and +ensure all fields are up to date before exiting the session. + +```python +class AddUserForm(rx.State): + user: User | None = None + + @rx.event + def add_user(self, form_data: dict[str, Any]): + with rx.session() as session: + self.user = User(**form_data) + session.add(self.user) + session.commit() + session.refresh(self.user) +``` + +Now the `self.user` object will have a correct reference to the autogenerated +primary key, `id`, even though this was not provided when the object was created +from the form data. + +If `self.user` needs to be modified or used in another query in a new session, +it must be added to the session. Adding an object to a session does not +necessarily create the object, but rather associates it with a session where it +may either be created or updated accordingly. + +```python +class AddUserForm(rx.State): + ... + + @rx.event + def update_user(self, form_data: dict[str, Any]): + if self.user is None: + return + with rx.session() as session: + self.user.set(**form_data) + session.add(self.user) + session.commit() + session.refresh(self.user) +``` + +If an ORM object will be referenced and accessed outside of a session, you +should call `.refresh()` on it to avoid stale object exceptions. + +## Using SQL Directly + +Avoiding SQL is one of the main benefits of using an ORM, but sometimes it is +necessary for particularly complex queries, or when using database-specific +features. + +SQLModel exposes the `session.execute()` method that can be used to execute raw +SQL strings. If parameter binding is needed, the query may be wrapped in +[`sqlalchemy.text`](https://docs.sqlalchemy.org/en/14/core/sqlelement.html#sqlalchemy.sql.expression.text), +which allows colon-prefix names to be used as placeholders. + +```md alert info +# Never use string formatting to construct SQL queries, as this may lead to SQL injection vulnerabilities in the app. +``` + +```python +import sqlalchemy + +import reflex as rx + + +class State(rx.State): + + @rx.event + def insert_user_raw(self, username, email): + with rx.session() as session: + session.execute( + sqlalchemy.text( + "INSERT INTO user (username, email) " + "VALUES (:username, :email)" + ), + \{"username": username, "email": email}, + ) + session.commit() + + @rx.var + def raw_user_tuples(self) -> list[list]: + with rx.session() as session: + return [list(row) for row in session.execute("SELECT * FROM user").all()] +``` + +## Async Database Operations + +Reflex provides an async version of the session function called `rx.asession` for asynchronous database operations. This is useful when you need to perform database operations in an async context, such as within async event handlers. + +The `rx.asession` function returns an async SQLAlchemy session that must be used with an async context manager. Most operations against the `asession` must be awaited. + +```python +import sqlalchemy.ext.asyncio +import sqlalchemy + +import reflex as rx + + +class AsyncUserState(rx.State): + users: list[User] = [] + + @rx.event(background=True) + async def get_users_async(self): + async with rx.asession() as asession: + result = await asession.execute(User.select()) + async with self: + self.users = result.all() +``` + +### Async Select + +The following example shows how to query the database asynchronously: + +```python +class AsyncQueryUser(rx.State): + name: str + users: list[User] = [] + + @rx.event(background=True) + async def get_users(self): + async with rx.asession() as asession: + stmt = User.select().where(User.username.contains(self.name)) + result = await asession.execute(stmt) + async with self: + self.users = result.all() +``` + +### Async Insert + +To add a new record to the database asynchronously: + +```python +class AsyncAddUser(rx.State): + username: str + email: str + + @rx.event(background=True) + async def add_user(self): + async with rx.asession() as asession: + asession.add(User(username=self.username, email=self.email)) + await asession.commit() +``` + +### Async Update + +To update a user asynchronously: + +```python +class AsyncChangeEmail(rx.State): + username: str + email: str + + @rx.event(background=True) + async def modify_user(self): + async with rx.asession() as asession: + stmt = User.select().where(User.username == self.username) + result = await asession.execute(stmt) + user = result.first() + if user: + user.email = self.email + asession.add(user) + await asession.commit() +``` + +### Async Delete + +To delete a user asynchronously: + +```python +class AsyncRemoveUser(rx.State): + username: str + + @rx.event(background=True) + async def delete_user(self): + async with rx.asession() as asession: + stmt = User.select().where(User.username == self.username) + result = await asession.execute(stmt) + user = result.first() + if user: + await asession.delete(user) + await asession.commit() +``` + +### Async Refresh + +Similar to the regular session, you can refresh an object to ensure all fields are up to date: + +```python +class AsyncAddUserForm(rx.State): + user: User | None = None + + @rx.event(background=True) + async def add_user(self, form_data: dict[str, str]): + async with rx.asession() as asession: + async with self: + self.user = User(**form_data) + asession.add(self.user) + await asession.commit() + await asession.refresh(self.user) +``` + +### Async SQL Execution + +You can also execute raw SQL asynchronously: + +```python +class AsyncRawSQL(rx.State): + users: list[list] = [] + + @rx.event(background=True) + async def insert_user_raw(self, username, email): + async with rx.asession() as asession: + await asession.execute( + sqlalchemy.text( + "INSERT INTO user (username, email) " + "VALUES (:username, :email)" + ), + dict(username=username, email=email), + ) + await asession.commit() + + @rx.event(background=True) + async def get_raw_users(self): + async with rx.asession() as asession: + result = await asession.execute("SELECT * FROM user") + async with self: + self.users = [list(row) for row in result.all()] +``` + +```md alert info +# Important Notes for Async Database Operations + +- Always use the `@rx.event(background=True)` decorator for async event handlers +- Most operations against the `asession` must be awaited, including `commit()`, `execute()`, `refresh()`, and `delete()` +- The `add()` method does not need to be awaited +- Result objects from queries have methods like `all()` and `first()` that are synchronous and return data directly +- Use `async with self:` when updating state variables in background tasks +``` diff --git a/docs/database/relationships.md b/docs/database/relationships.md new file mode 100644 index 00000000000..93bfb53c5aa --- /dev/null +++ b/docs/database/relationships.md @@ -0,0 +1,164 @@ +# Relationships + +Foreign key relationships are used to link two tables together. For example, +the `Post` model may have a field, `user_id`, with a foreign key of `user.id`, +referencing a `User` model. This would allow us to automatically query the `Post` objects +associated with a user, or find the `User` object associated with a `Post`. + +To establish bidirectional relationships a model must correctly set the +`back_populates` keyword argument on the `Relationship` to the relationship +attribute in the _other_ model. + +## Foreign Key Relationships + +To create a relationship, first add a field to the model that references the +primary key of the related table, then add a `sqlmodel.Relationship` attribute +which can be used to access the related objects. + +Defining relationships like this requires the use of `sqlmodel` objects as +seen in the example. + +```python +from typing import List, Optional + +import sqlmodel + +import reflex as rx + + +class Post(rx.Model, table=True): + title: str + body: str + user_id: int = sqlmodel.Field(foreign_key="user.id") + + user: Optional["User"] = sqlmodel.Relationship(back_populates="posts") + flags: Optional[List["Flag"]] = sqlmodel.Relationship(back_populates="post") + + +class User(rx.Model, table=True): + username: str + email: str + + posts: List[Post] = sqlmodel.Relationship(back_populates="user") + flags: List["Flag"] = sqlmodel.Relationship(back_populates="user") + + +class Flag(rx.Model, table=True): + post_id: int = sqlmodel.Field(foreign_key="post.id") + user_id: int = sqlmodel.Field(foreign_key="user.id") + message: str + + post: Optional[Post] = sqlmodel.Relationship(back_populates="flags") + user: Optional[User] = sqlmodel.Relationship(back_populates="flags") +``` + +See the [SQLModel Relationship Docs](https://sqlmodel.tiangolo.com/tutorial/relationship-attributes/define-relationships-attributes/) for more details. + +## Querying Relationships + +### Inserting Linked Objects + +The following example assumes that the flagging user is stored in the state as a +`User` instance and that the post `id` is provided in the data submitted in the +form. + +```python +class FlagPostForm(rx.State): + user: User + + @rx.event + def flag_post(self, form_data: dict[str, Any]): + with rx.session() as session: + post = session.get(Post, int(form_data.pop("post_id"))) + flag = Flag(message=form_data.pop("message"), post=post, user=self.user) + session.add(flag) + session.commit() +``` + +### How are Relationships Dereferenced? + +By default, the relationship attributes are in **lazy loading** or `"select"` +mode, which generates a query _on access_ to the relationship attribute. Lazy +loading is generally fine for single object lookups and manipulation, but can be +inefficient when accessing many linked objects for serialization purposes. + +There are several alternative loading mechanisms available that can be set on +the relationship object or when performing the query. + +- "joined" or `joinload` - generates a single query to load all related objects + at once. +- "subquery" or `subqueryload` - generates a single query to load all related + objects at once, but uses a subquery to do the join, instead of a join in the + main query. +- "selectin" or `selectinload` - emits a second (or more) SELECT statement which + assembles the primary key identifiers of the parent objects into an IN clause, + so that all members of related collections / scalar references are loaded at + once by primary key + +There are also non-loading mechanisms, "raise" and "noload" which are used to +specifically avoid loading a relationship. + +Each loading method comes with tradeoffs and some are better suited for different +data access patterns. +See [SQLAlchemy: Relationship Loading Techniques](https://docs.sqlalchemy.org/en/14/orm/loading_relationships.html) +for more detail. + +### Querying Linked Objects + +To query the `Post` table and include all `User` and `Flag` objects up front, +the `.options` interface will be used to specify `selectinload` for the required +relationships. Using this method, the linked objects will be available for +rendering in frontend code without additional steps. + +```python +import sqlalchemy + + +class PostState(rx.State): + posts: List[Post] + + @rx.event + def load_posts(self): + with rx.session() as session: + self.posts = session.exec( + Post.select + .options( + sqlalchemy.orm.selectinload(Post.user), + sqlalchemy.orm.selectinload(Post.flags).options( + sqlalchemy.orm.selectinload(Flag.user), + ), + ) + .limit(15) + ).all() +``` + +The loading methods create new query objects and thus may be linked if the +relationship itself has other relationships that need to be loaded. In this +example, since `Flag` references `User`, the `Flag.user` relationship must be +chain loaded from the `Post.flags` relationship. + +### Specifying the Loading Mechanism on the Relationship + +Alternatively, the loading mechanism can be specified on the relationship by +passing `sa_relationship_kwargs=\{"lazy": method}` to `sqlmodel.Relationship`, +which will use the given loading mechanism in all queries by default. + +```python +from typing import List, Optional + +import sqlmodel + +import reflex as rx + + +class Post(rx.Model, table=True): + ... + user: Optional["User"] = sqlmodel.Relationship( + back_populates="posts", + sa_relationship_kwargs=\{"lazy": "selectin"}, + ) + flags: Optional[List["Flag"]] = sqlmodel.Relationship( + back_populates="post", + sa_relationship_kwargs=\{"lazy": "selectin"}, + ) +``` diff --git a/docs/database/tables.md b/docs/database/tables.md new file mode 100644 index 00000000000..4be4b164540 --- /dev/null +++ b/docs/database/tables.md @@ -0,0 +1,70 @@ +# Tables + +Tables are database objects that contain all the data in a database. + +In tables, data is logically organized in a row-and-column format similar to a +spreadsheet. Each row represents a unique record, and each column represents a +field in the record. + +## Creating a Table + +To create a table, make a class that inherits from `rx.Model`. + +The following example shows how to create a table called `User`. + +```python +class User(rx.Model, table=True): + username: str + email: str +``` + +The `table=True` argument tells Reflex to create a table in the database for +this class. + +### Primary Key + +By default, Reflex will create a primary key column called `id` for each table. + +However, if an `rx.Model` defines a different field with `primary_key=True`, then the +default `id` field will not be created. A table may also redefine `id` as needed. + +It is not currently possible to create a table without a primary key. + +## Advanced Column Types + +SQLModel automatically maps basic python types to SQLAlchemy column types, but +for more advanced use cases, it is possible to define the column type using +`sqlalchemy` directly. For example, we can add a last updated timestamp to the +post example as a proper `DateTime` field with timezone. + +```python +import datetime + +import sqlmodel +import sqlalchemy + +class Post(rx.Model, table=True): + ... + update_ts: datetime.datetime = sqlmodel.Field( + default=None, + sa_column=sqlalchemy.Column( + "update_ts", + sqlalchemy.DateTime(timezone=True), + server_default=sqlalchemy.func.now(), + ), + ) +``` + +To make the `Post` model more usable on the frontend, a `dict` method may be provided +that converts any fields to a JSON serializable value. In this case, the dict method is +overriding the default `datetime` serializer to strip off the microsecond part. + +```python +class Post(rx.Model, table=True): + ... + + def dict(self, *args, **kwargs) -> dict: + d = super().dict(*args, **kwargs) + d["update_ts"] = self.update_ts.replace(microsecond=0).isoformat() + return d +``` diff --git a/docs/de/README.md b/docs/de/README.md deleted file mode 100644 index 3ce5c6c02f8..00000000000 --- a/docs/de/README.md +++ /dev/null @@ -1,250 +0,0 @@ -
-Reflex Logo -
- -### **✨ Performante, anpassbare Web-Apps in purem Python. Bereitstellung in Sekunden. ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) - -
- ---- - -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex ist eine Bibliothek, mit der man Full-Stack-Web-Applikationen in purem Python erstellen kann. - -Wesentliche Merkmale: - -- **Pures Python** - Schreibe dein Front- und Backend in Python, es gibt also keinen Grund, JavaScript zu lernen. -- **Volle Flexibilität** - Reflex ist einfach zu handhaben, kann aber auch für komplexe Anwendungen skaliert werden. -- **Sofortige Bereitstellung** - Nach dem Erstellen kannst du deine App mit einem [einzigen Befehl](https://reflex.dev/docs/hosting/deploy-quick-start/) bereitstellen oder auf deinem eigenen Server hosten. - -Auf unserer [Architektur-Seite](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) erfahren Sie, wie Reflex unter der Haube funktioniert. - -## ⚙️ Installation - -Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.10+): - -```bash -pip install reflex -``` - -## 🥳 Erstelle deine erste App - -Die Installation von `reflex` installiert auch das `reflex`-Kommandozeilen-Tool. - -Teste, ob die Installation erfolgreich war, indem du ein neues Projekt erstellst. (Ersetze `my_app_name` durch deinen Projektnamen): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -Dieser Befehl initialisiert eine Vorlage in deinem neuen Verzeichnis. - -Du kannst diese App im Entwicklungsmodus ausführen: - -```bash -reflex run -``` - -Du solltest deine App unter http://localhost:3000 laufen sehen. - -Nun kannst du den Quellcode in `my_app_name/my_app_name.py` ändern. Reflex hat schnelle Aktualisierungen, sodass du deine Änderungen sofort siehst, wenn du deinen Code speicherst. - -## 🫧 Beispiel-App - -Lass uns ein Beispiel durchgehen: die Erstellung einer Benutzeroberfläche für die Bildgenerierung mit [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). Zur Vereinfachung rufen wir einfach die [OpenAI-API](https://platform.openai.com/docs/api-reference/authentication) auf, aber du könntest dies auch durch ein lokal ausgeführtes ML-Modell ersetzen. - -  - -
-Eine Benutzeroberfläche für DALL·E, die im Prozess der Bildgenerierung gezeigt wird. -
- -  - -Hier ist der komplette Code, um dies zu erstellen. Das alles wird in einer Python-Datei gemacht! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """Der Zustand der App.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Hole das Bild aus dem Prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True - - -def index(): - return rx.center( - rx.vstack( - rx.heading("DALL-E", font_size="1.5em"), - rx.input( - placeholder="Enter a prompt..", - on_blur=State.set_prompt, - width="25em", - ), - rx.button( - "Generate Image", - on_click=State.get_image, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Füge Zustand und Seite zur App hinzu. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## Schauen wir uns das mal genauer an. - -
-Erläuterung der Unterschiede zwischen Backend- und Frontend-Teilen der DALL-E-App. -
- -### **Reflex-UI** - -Fangen wir mit der Benutzeroberfläche an. - -```python -def index(): - return rx.center( - ... - ) -``` - -Diese `index`-Funktion definiert das Frontend der App. - -Wir verwenden verschiedene Komponenten wie `center`, `vstack`, `input` und `button`, um das Frontend zu erstellen. Komponenten können ineinander verschachtelt werden, um komplexe Layouts zu erstellen. Und du kannst Schlüsselwortargumente verwenden, um sie mit der vollen Kraft von CSS zu stylen. - -Reflex wird mit [über 60 eingebauten Komponenten](https://reflex.dev/docs/library) geliefert, die dir den Einstieg erleichtern. Wir fügen aktiv weitere Komponenten hinzu, und es ist einfach, [eigene Komponenten zu erstellen](https://reflex.dev/docs/wrapping-react/overview/). - -### **State** - -Reflex stellt deine Benutzeroberfläche als Funktion deines Zustands dar. - -```python -class State(rx.State): - """Der Zustand der App.""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -Der Zustand definiert alle Variablen (genannt Vars) in einer App, die sich ändern können, und die Funktionen, die sie ändern. - -Hier besteht der Zustand aus einem `prompt` und einer `image_url`. Es gibt auch die Booleans `processing` und `complete`, um anzuzeigen, wann der Button deaktiviert werden soll (während der Bildgenerierung) und wann das resultierende Bild angezeigt werden soll. - -### **Event-Handler** - -```python -def get_image(self): - """Hole das Bild aus dem Prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True -``` - -Innerhalb des Zustands definieren wir Funktionen, die als Event-Handler bezeichnet werden und die Zustand-Variablen ändern. Event-Handler sind die Art und Weise, wie wir den Zustand in Reflex ändern können. Sie können als Reaktion auf Benutzeraktionen aufgerufen werden, z.B. beim Klicken auf eine Schaltfläche oder bei der Eingabe in ein Textfeld. Diese Aktionen werden als Ereignisse bezeichnet. - -Unsere DALL-E.-App hat einen Event-Handler, `get_image`, der dieses Bild von der OpenAI-API abruft. Die Verwendung von `yield` in der Mitte eines Event-Handlers führt zu einer Aktualisierung der Benutzeroberfläche. Andernfalls wird die Benutzeroberfläche am Ende des Ereignishandlers aktualisiert. - -### **Routing** - -Schließlich definieren wir unsere App. - -```python -app = rx.App() -``` - -Wir fügen der Indexkomponente eine Seite aus dem Stammverzeichnis der Anwendung hinzu. Wir fügen auch einen Titel hinzu, der in der Seitenvorschau/Browser-Registerkarte angezeigt wird. - -```python -app.add_page(index, title="DALL-E") -``` - -Du kannst eine mehrseitige App erstellen, indem du weitere Seiten hinzufügst. - -## 📑 Ressourcen - -
- -📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Komponentenbibliothek](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Bereitstellung](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Status - -Reflex wurde im Dezember 2022 unter dem Namen Pynecone gestartet. - -Ab 2025 wurde [Reflex Cloud](https://cloud.reflex.dev) gestartet, um die beste Hosting-Erfahrung für Reflex-Apps zu bieten. Wir werden es weiterhin entwickeln und mehr Funktionen implementieren. - -Reflex hat wöchentliche Veröffentlichungen und neue Features! Stelle sicher, dass du dieses Repository mit einem :star: Stern markierst und :eyes: beobachtest, um auf dem Laufenden zu bleiben. - -## Beitragende - -Wir begrüßen Beiträge jeder Größe! Hier sind einige gute Möglichkeiten, um in der Reflex-Community zu starten. - -- **Tritt unserem Discord bei**: Unser [Discord](https://discord.gg/T5WSbC2YtQ) ist der beste Ort, um Hilfe für dein Reflex-Projekt zu bekommen und zu besprechen, wie du beitragen kannst. -- **GitHub-Diskussionen**: Eine großartige Möglichkeit, über Funktionen zu sprechen, die du hinzugefügt haben möchtest oder Dinge, die verwirrend sind/geklärt werden müssen. -- **GitHub-Issues**: [Issues](https://github.com/reflex-dev/reflex/issues) sind eine ausgezeichnete Möglichkeit, Bugs zu melden. Außerdem kannst du versuchen, ein bestehendes Problem zu lösen und eine PR einzureichen. - -Wir suchen aktiv nach Mitwirkenden, unabhängig von deinem Erfahrungslevel oder deiner Erfahrung. Um beizutragen, sieh dir [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) an. - -## Vielen Dank an unsere Mitwirkenden: - - - - - -## Lizenz - -Reflex ist Open-Source und lizenziert unter der [Apache License 2.0](/LICENSE). diff --git a/docs/es/README.md b/docs/es/README.md deleted file mode 100644 index 9f9ab69d628..00000000000 --- a/docs/es/README.md +++ /dev/null @@ -1,247 +0,0 @@ -
-Reflex Logo -
- -### **✨ Aplicaciones web personalizables y eficaces en Python puro. Despliega tu aplicación en segundos. ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![Versiones](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentación](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) - -
- ---- - -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex es una biblioteca para construir aplicaciones web full-stack en Python puro. - -Características clave: - -- **Python puro** - Escribe el frontend y backend de tu aplicación en Python, sin necesidad de aprender JavaScript. -- **Flexibilidad total** - Reflex es fácil para empezar, pero también puede escalar a aplicaciones complejas. -- **Despliegue instantáneo** - Después de construir, despliega tu aplicación con un [solo comando](https://reflex.dev/docs/hosting/deploy-quick-start/) u hospédala en tu propio servidor. - -Consulta nuestra [página de arquitectura](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) para aprender cómo funciona Reflex en detalle. - -## ⚙️ Instalación - -Abra un terminal y ejecute (Requiere Python 3.10+): - -```bash -pip install reflex -``` - -## 🥳 Crea tu primera aplicación - -Al instalar `reflex` también se instala la herramienta de línea de comandos `reflex`. - -Compruebe que la instalación se ha realizado correctamente creando un nuevo proyecto. (Sustituye `my_app_name` por el nombre de tu proyecto): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -Este comando inicializa una plantilla en tu nuevo directorio. - -Puedes iniciar esta aplicación en modo de desarrollo: - -```bash -reflex run -``` - -Debería ver su aplicación ejecutándose en http://localhost:3000. - -Ahora puede modificar el código fuente en `my_app_name/my_app_name.py`. Reflex se actualiza rápidamente para que pueda ver los cambios al instante cuando guarde el código. - -## 🫧 Ejemplo de una Aplicación - -Veamos un ejemplo: crearemos una UI de generación de imágenes en torno a [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). Para simplificar, solo llamamos a la [API de OpenAI](https://platform.openai.com/docs/api-reference/authentication), pero podrías reemplazar esto con un modelo ML ejecutado localmente. - -  - -
-Un envoltorio frontend para DALL·E, mostrado en el proceso de generar una imagen. -
- -  - -Aquí está el código completo para crear esto. ¡Todo esto se hace en un archivo de Python! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - -class State(rx.State): - """El estado de la aplicación""" - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Obtiene la imagen desde la consulta.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True - - -def index(): - return rx.center( - rx.vstack( - rx.heading("DALL-E", font_size="1.5em"), - rx.input( - placeholder="Enter a prompt..", - on_blur=State.set_prompt, - width="25em", - ), - rx.button( - "Generate Image", - on_click=State.get_image, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Agrega el estado y la pagina a la aplicación -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## Vamos a analizarlo. - -
-Explicando las diferencias entre las partes del backend y frontend de la aplicación DALL-E. -
- -### **Reflex UI** - -Empezemos por la interfaz de usuario (UI). - -```python -def index(): - return rx.center( - ... - ) -``` - -Esta función `index` define el frontend de la aplicación. - -Utilizamos diferentes componentes como `center`, `vstack`, `input`, y `button` para construir el frontend. Los componentes pueden anidarse unos dentro de otros para crear diseños complejos. Además, puedes usar argumentos de tipo keyword para darles estilo con toda la potencia de CSS. - -Reflex viene con [mas de 60 componentes incorporados](https://reflex.dev/docs/library) para ayudarle a empezar. Estamos añadiendo activamente más componentes y es fácil [crear sus propios componentes](https://reflex.dev/docs/wrapping-react/overview/). - -### **Estado** - -Reflex representa su UI como una función de su estado (State). - -```python -class State(rx.State): - """El estado de la aplicación""" - prompt = "" - image_url = "" - processing = False - complete = False -``` - -El estado (State) define todas las variables (llamadas vars) de una aplicación que pueden cambiar y las funciones que las modifican. - -Aquí el estado se compone de `prompt` e `image_url`. También están los booleanos `processing` y `complete` para indicar cuando se deshabilite el botón (durante la generación de la imagen) y cuando se muestre la imagen resultante. - -### **Manejadores de Evento** - -```python -def get_image(self): - """Obtiene la imagen desde la consulta.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True -``` - -Dentro del estado, definimos funciones llamadas manejadores de eventos que cambian las variables de estado. Los Manejadores de Evento son la manera que podemos modificar el estado en Reflex. Pueden ser activados en respuesta a las acciones del usuario, como hacer clic en un botón o escribir en un cuadro de texto. Estas acciones se llaman eventos. - -Nuestra aplicación DALL·E tiene un manipulador de eventos, `get_image` que recibe esta imagen del OpenAI API. El uso de `yield` en medio de un manipulador de eventos hará que la UI se actualice. De lo contrario, la interfaz se actualizará al final del manejador de eventos. - -### **Enrutamiento** - -Por último, definimos nuestra app. - -```python -app = rx.App() -``` - -Añadimos una página desde la raíz (root) de la aplicación al componente de índice (index). También agregamos un título que se mostrará en la vista previa de la página/pestaña del navegador. - -```python -app.add_page(index, title="DALL-E") -``` - -Puedes crear una aplicación multipágina añadiendo más páginas. - -## 📑 Recursos - -
- -📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Librería de componentes](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Despliegue](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Estado - -Reflex se lanzó en diciembre de 2022 con el nombre de Pynecone. - -A partir de 2025, [Reflex Cloud](https://cloud.reflex.dev) se ha lanzado para proporcionar la mejor experiencia de alojamiento para aplicaciones Reflex. Continuaremos desarrollándolo e implementando más características. - -¡Reflex tiene nuevas versiones y características cada semana! Asegúrate de :star: marcar como favorito y :eyes: seguir este repositorio para mantenerte actualizado. - -## Contribuciones - -¡Aceptamos contribuciones de cualquier tamaño! A continuación encontrará algunas buenas formas de iniciarse en la comunidad Reflex. - -- **Únete a nuestro Discord**: Nuestro [Discord](https://discord.gg/T5WSbC2YtQ) es el mejor lugar para obtener ayuda en su proyecto Reflex y discutir cómo puedes contribuir. -- **Discusiones de GitHub**: Una excelente manera de hablar sobre las características que deseas agregar o las cosas que te resultan confusas o necesitan aclaración. -- **GitHub Issues**: Las incidencias son una forma excelente de informar de errores. Además, puedes intentar resolver un problema existente y enviar un PR. - -Buscamos colaboradores, sin importar su nivel o experiencia. Para contribuir consulta [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) - -## Todo Gracias A Nuestros Contribuidores: - - - - - -## Licencia - -Reflex es de código abierto y está licenciado bajo la [Apache License 2.0](/LICENSE). diff --git a/docs/events/background_events.md b/docs/events/background_events.md new file mode 100644 index 00000000000..04d381cb7d0 --- /dev/null +++ b/docs/events/background_events.md @@ -0,0 +1,157 @@ +```python exec +import reflex as rx +from pcweb import constants, styles +``` + +# Background Tasks + +A background task is a special type of `EventHandler` that may run +concurrently with other `EventHandler` functions. This enables long-running +tasks to execute without blocking UI interactivity. + +A background task is defined by decorating an async `State` method with +`@rx.event(background=True)`. + +```md alert warning +# `@rx.event(background=True)` used to be called `@rx.background`. + +In Reflex version 0.6.5 and later, the `@rx.background` decorator has been renamed to `@rx.event(background=True)`. +``` + +Whenever a background task needs to interact with the state, **it must enter an +`async with self` context block** which refreshes the state and takes an +exclusive lock to prevent other tasks or event handlers from modifying it +concurrently. Because other `EventHandler` functions may modify state while the +task is running, **outside of the context block, Vars accessed by the background +task may be _stale_**. Attempting to modify the state from a background task +outside of the context block will raise an `ImmutableStateError` exception. + +In the following example, the `my_task` event handler is decorated with +`@rx.event(background=True)` and increments the `counter` variable every half second, as +long as certain conditions are met. While it is running, the UI remains +interactive and continues to process events normally. + +```md alert info +# Background events are similar to simple Task Queues like [Celery](https://www.fullstackpython.com/celery.html) allowing asynchronous events. +``` + +```python demo exec id=background_demo +import asyncio +import reflex as rx + + +class MyTaskState(rx.State): + counter: int = 0 + max_counter: int = 10 + running: bool = False + _n_tasks: int = 0 + + @rx.event + def set_max_counter(self, value: str): + self.max_counter = int(value) + + @rx.event(background=True) + async def my_task(self): + async with self: + # The latest state values are always available inside the context + if self._n_tasks > 0: + # only allow 1 concurrent task + return + + # State mutation is only allowed inside context block + self._n_tasks += 1 + + while True: + async with self: + # Check for stopping conditions inside context + if self.counter >= self.max_counter: + self.running = False + if not self.running: + self._n_tasks -= 1 + return + + self.counter += 1 + + # Await long operations outside the context to avoid blocking UI + await asyncio.sleep(0.5) + + @rx.event + def toggle_running(self): + self.running = not self.running + if self.running: + return MyTaskState.my_task + + @rx.event + def clear_counter(self): + self.counter = 0 + + +def background_task_example(): + return rx.hstack( + rx.heading(MyTaskState.counter, " /"), + rx.input( + value=MyTaskState.max_counter, + on_change=MyTaskState.set_max_counter, + width="8em", + ), + rx.button( + rx.cond(~MyTaskState.running, "Start", "Stop"), + on_click=MyTaskState.toggle_running, + ), + rx.button( + "Reset", + on_click=MyTaskState.clear_counter, + ), + ) +``` + +## Terminating Background Tasks on Page Close or Navigation + +Sometimes, background tasks may continue running even after the user navigates away from the page or closes the browser tab. To handle such cases, you can check if the websocket associated with the state is disconnected and terminate the background task when necessary. + +The solution involves checking if the client_token is still valid in the app.event_namespace.token_to_sid mapping. If the session is lost (e.g., the user navigates away or closes the page), the background task will stop. + +```python +import asyncio +import reflex as rx + +class State(rx.State): + @rx.event(background=True) + async def loop_function(self): + while True: + if self.router.session.client_token not in app.event_namespace.token_to_sid: + print("WebSocket connection closed or user navigated away. Stopping background task.") + break + + print("Running background task...") + await asyncio.sleep(2) + + +@rx.page(on_load=State.loop_function) +def index(): + return rx.text("Hello, this page will manage background tasks and stop them when the page is closed or navigated away.") + +``` + +## Task Lifecycle + +When a background task is triggered, it starts immediately, saving a reference to +the task in `app.background_tasks`. When the task completes, it is removed from +the set. + +Multiple instances of the same background task may run concurrently, and the +framework makes no attempt to avoid duplicate tasks from starting. + +It is up to the developer to ensure that duplicate tasks are not created under +the circumstances that are undesirable. In the example above, the `_n_tasks` +backend var is used to control whether `my_task` will enter the increment loop, +or exit early. + +## Background Task Limitations + +Background tasks mostly work like normal `EventHandler` methods, with certain exceptions: + +- Background tasks must be `async` functions. +- Background tasks cannot modify the state outside of an `async with self` context block. +- Background tasks may read the state outside of an `async with self` context block, but the value may be stale. +- Background tasks may not be directly called from other event handlers or background tasks. Instead use `yield` or `return` to trigger the background task. diff --git a/docs/events/chaining_events.md b/docs/events/chaining_events.md new file mode 100644 index 00000000000..1d14ff16eee --- /dev/null +++ b/docs/events/chaining_events.md @@ -0,0 +1,94 @@ +```python exec +import reflex as rx +``` + +# Chaining events + +## Calling Event Handlers From Event Handlers + +You can call other event handlers from event handlers to keep your code modular. Just use the `self.call_handler` syntax to run another event handler. As always, you can yield within your function to send incremental updates to the frontend. + +```python demo exec id=call-handler +import asyncio + +class CallHandlerState(rx.State): + count: int = 0 + progress: int = 0 + + @rx.event + async def run(self): + # Reset the count. + self.set_progress(0) + yield + + # Count to 10 while showing progress. + for i in range(10): + # Wait and increment. + await asyncio.sleep(0.5) + self.count += 1 + + # Update the progress. + self.set_progress(i + 1) + + # Yield to send the update. + yield + + +def call_handler_example(): + return rx.vstack( + rx.badge(CallHandlerState.count, font_size="1.5em", color_scheme="green"), + rx.progress(value=CallHandlerState.progress, max=10, width="100%"), + rx.button("Run", on_click=CallHandlerState.run), + ) +``` + +## Returning Events From Event Handlers + +So far, we have only seen events that are triggered by components. However, an event handler can also return events. + +In Reflex, event handlers run synchronously, so only one event handler can run at a time, and the events in the queue will be blocked until the current event handler finishes.The difference between returning an event and calling an event handler is that returning an event will send the event to the frontend and unblock the queue. + +```md alert info +# Be sure to use the class name `State` (or any substate) rather than `self` when returning events. +``` + +Try entering an integer in the input below then clicking out. + +```python demo exec id=collatz +class CollatzState(rx.State): + count: int = 1 + + @rx.event + def start_collatz(self, count: str): + """Run the collatz conjecture on the given number.""" + self.count = abs(int(count if count else 1)) + return CollatzState.run_step + + @rx.event + async def run_step(self): + """Run a single step of the collatz conjecture.""" + + while self.count > 1: + + await asyncio.sleep(0.5) + + if self.count % 2 == 0: + # If the number is even, divide by 2. + self.count //= 2 + else: + # If the number is odd, multiply by 3 and add 1. + self.count = self.count * 3 + 1 + yield + + +def collatz_example(): + return rx.vstack( + rx.badge(CollatzState.count, font_size="1.5em", color_scheme="green"), + rx.input(on_blur=CollatzState.start_collatz), + ) + +``` + +In this example, we run the [Collatz Conjecture](https://en.wikipedia.org/wiki/Collatz_conjecture) on a number entered by the user. + +When the `on_blur` event is triggered, the event handler `start_collatz` is called. It sets the initial count, then calls `run_step` which runs until the count reaches `1`. diff --git a/docs/events/decentralized_event_handlers.md b/docs/events/decentralized_event_handlers.md new file mode 100644 index 00000000000..c7a20029cd0 --- /dev/null +++ b/docs/events/decentralized_event_handlers.md @@ -0,0 +1,152 @@ +```python exec +import reflex as rx +``` + +# Decentralized Event Handlers + +## Overview + +Decentralized event handlers allow you to define event handlers outside of state classes, providing more flexible code organization. This feature was introduced in Reflex v0.7.10 and enables a more modular approach to event handling. + +With decentralized event handlers, you can: + +- Organize event handlers by feature rather than by state class +- Separate UI logic from state management +- Create more maintainable and scalable applications + +## Basic Usage + +To create a decentralized event handler, use the `@rx.event` decorator on a function that takes a state instance as its first parameter: + +```python demo exec +import reflex as rx + +class MyState(rx.State): + count: int = 0 + +@rx.event +def increment(state: MyState, amount: int): + state.count += amount + +def decentralized_event_example(): + return rx.vstack( + rx.heading(f"Count: {MyState.count}"), + rx.hstack( + rx.button("Increment by 1", on_click=increment(1)), + rx.button("Increment by 5", on_click=increment(5)), + rx.button("Increment by 10", on_click=increment(10)), + ), + spacing="4", + align="center", + ) +``` + +In this example: + +1. We define a `MyState` class with a `count` variable +2. We create a decentralized event handler `increment` that takes a `MyState` instance as its first parameter +3. We use the event handler in buttons, passing different amounts to increment by + +## Compared to Traditional Event Handlers + +Here's a comparison between traditional event handlers defined within state classes and decentralized event handlers: + +```python box +# Traditional event handler within a state class +class TraditionalState(rx.State): + count: int = 0 + + @rx.event + def increment(self, amount: int = 1): + self.count += amount + +# Usage in components +rx.button("Increment", on_click=TraditionalState.increment(5)) + +# Decentralized event handler outside the state class +class DecentralizedState(rx.State): + count: int = 0 + +@rx.event +def increment(state: DecentralizedState, amount: int = 1): + state.count += amount + +# Usage in components +rx.button("Increment", on_click=increment(5)) +``` + +Key differences: + +- Traditional event handlers use `self` to reference the state instance +- Decentralized event handlers explicitly take a state instance as the first parameter +- Both approaches use the same syntax for triggering events in components +- Both can be decorated with `@rx.event` respectively + +## Best Practices + +### When to Use Decentralized Event Handlers + +Decentralized event handlers are particularly useful in these scenarios: + +1. **Large applications** with many event handlers that benefit from better organization +2. **Feature-based organization** where you want to group related event handlers together +3. **Separation of concerns** when you want to keep state definitions clean and focused + +### Type Annotations + +Always use proper type annotations for your state parameter and any additional parameters: + +```python box +@rx.event +def update_user(state: UserState, name: str, age: int): + state.name = name + state.age = age +``` + +### Naming Conventions + +Follow these naming conventions for clarity: + +1. Use descriptive names that indicate the action being performed +2. Use the state class name as the type annotation for the first parameter +3. Name the state parameter consistently across your codebase (e.g., always use `state` or the first letter of the state class) + +### Organization + +Consider these approaches for organizing decentralized event handlers: + +1. Group related event handlers in the same file +2. Place event handlers near the state classes they modify +3. For larger applications, create a dedicated `events` directory with files organized by feature + +```python box +# Example organization in a larger application +# events/user_events.py +@rx.event +def update_user(state: UserState, name: str, age: int): + state.name = name + state.age = age + +@rx.event +def delete_user(state: UserState): + state.name = "" + state.age = 0 +``` + +### Combining with Other Event Features + +Decentralized event handlers work seamlessly with other Reflex event features: + +```python box +# Background event +@rx.event(background=True) +async def long_running_task(state: AppState): + # Long-running task implementation + pass + +# Event chaining +@rx.event +def process_form(state: FormState, data: dict): + # Process form data + return validate_data # Chain to another event +``` diff --git a/docs/events/event_actions.md b/docs/events/event_actions.md new file mode 100644 index 00000000000..1d431eddc8b --- /dev/null +++ b/docs/events/event_actions.md @@ -0,0 +1,252 @@ +```python exec +import reflex as rx +import datetime +``` + +# Event Actions + +In Reflex, an event action is a special behavior that occurs during or after +processing an event on the frontend. + +Event actions can modify how the browser handles DOM events or throttle and +debounce events before they are processed by the backend. + +An event action is specified by accessing attributes and methods present on all +EventHandlers and EventSpecs. + +## DOM Event Propagation + +_Added in v0.3.2_ + +### prevent_default + +The `.prevent_default` action prevents the default behavior of the browser for +the action. This action can be added to any existing event, or it can be used on its own by +specifying `rx.prevent_default` as an event handler. + +A common use case for this is to prevent navigation when clicking a link. + +```python demo +rx.link("This Link Does Nothing", href="https://reflex.dev/", on_click=rx.prevent_default) +``` + +```python demo exec +class LinkPreventDefaultState(rx.State): + status: bool = False + + @rx.event + def toggle_status(self): + self.status = not self.status + +def prevent_default_example(): + return rx.vstack( + rx.heading(f"The value is {LinkPreventDefaultState.status}"), + rx.link( + "Toggle Value", + href="https://reflex.dev/", + on_click=LinkPreventDefaultState.toggle_status.prevent_default, + ), + ) +``` + +### stop_propagation + +The `.stop_propagation` action stops the event from propagating to parent elements. + +This action is often used when a clickable element contains nested buttons that +should not trigger the parent element's click event. + +In the following example, the first button uses `.stop_propagation` to prevent +the click event from propagating to the outer vstack. The second button does not +use `.stop_propagation`, so the click event will also be handled by the on_click +attached to the outer vstack. + +```python demo exec +class StopPropagationState(rx.State): + where_clicked: list[str] = [] + + @rx.event + def handle_click(self, where: str): + self.where_clicked.append(where) + + @rx.event + def handle_reset(self): + self.where_clicked = [] + +def stop_propagation_example(): + return rx.vstack( + rx.button( + "btn1 - Stop Propagation", + on_click=StopPropagationState.handle_click("btn1").stop_propagation, + ), + rx.button( + "btn2 - Normal Propagation", + on_click=StopPropagationState.handle_click("btn2"), + ), + rx.foreach(StopPropagationState.where_clicked, rx.text), + rx.button( + "Reset", + on_click=StopPropagationState.handle_reset.stop_propagation, + ), + padding="2em", + border=f"1px dashed {rx.color('accent', 5)}", + on_click=StopPropagationState.handle_click("outer") + ) +``` + +## Throttling and Debounce + +_Added in v0.5.0_ + +For events that are fired frequently, it can be useful to throttle or debounce +them to avoid network latency and improve performance. These actions both take a +single argument which specifies the delay time in milliseconds. + +### throttle + +The `.throttle` action limits the number of times an event is processed within a +a given time period. It is useful for `on_scroll` and `on_mouse_move` events which are +fired very frequently, causing lag when handling them in the backend. + +```md alert warning +# Throttled events are discarded. + +There is no eventual delivery of any event that is triggered while the throttle +period is active. Throttle is not appropriate for events when the final payload +contains data that must be processed, like `on_change`. +``` + +In the following example, the `on_scroll` event is throttled to only fire every half second. + +```python demo exec +class ThrottleState(rx.State): + last_scroll: datetime.datetime | None + + @rx.event + def handle_scroll(self): + self.last_scroll = datetime.datetime.now(datetime.timezone.utc) + +def scroll_box(): + return rx.scroll_area( + rx.heading("Scroll Me"), + *[rx.text(f"Item {i}") for i in range(100)], + height="75px", + width="50%", + border=f"1px solid {rx.color('accent', 5)}", + on_scroll=ThrottleState.handle_scroll.throttle(500), + ) + +def throttle_example(): + return ( + scroll_box(), + rx.text( + f"Last Scroll Event: ", + rx.moment(ThrottleState.last_scroll, format="HH:mm:ss.SSS"), + ), + ) +``` + +```md alert info +# Event Actions are Chainable + +Event actions can be chained together to create more complex event handling +behavior. For example, you can throttle an event and prevent its default +behavior in the same event handler: `on_click=MyState.handle_click.throttle(500).prevent_default`. +``` + +### debounce + +The `.debounce` action delays the processing of an event until the specified +timeout occurs. If another event is triggered during the timeout, the timer is +reset and the original event is discarded. + +Debounce is useful for handling the final result of a series of events, such as +moving a slider. + +```md alert warning +# Debounced events are discarded. + +When a new event is triggered during the debounce period, the original event is +discarded. Debounce is not appropriate for events where each payload contains +unique data that must be processed, like `on_key_down`. +``` + +In the following example, the slider's `on_change` handler, `update_value`, is +only triggered on the backend when the slider value has not changed for half a +second. + +```python demo exec +class DebounceState(rx.State): + settled_value: int = 50 + + @rx.event + def update_value(self, value: list[int | float]): + self.settled_value = value[0] + + +def debounced_slider(): + return rx.slider( + key=rx.State.router.session.session_id, + default_value=[DebounceState.settled_value], + on_change=DebounceState.update_value.debounce(500), + width="100%", + ) + +def debounce_example(): + return rx.vstack( + debounced_slider(), + rx.text(f"Settled Value: {DebounceState.settled_value}"), + ) +``` + +```md alert info +# Why set key on the slider? + +Setting `key` to the `session_id` with a dynamic `default_value` ensures that +when the page is refreshed, the component will be re-rendered to reflect the +updated default_value from the state. + +Without the `key` set, the slider would always display the original +`settled_value` after a page reload, instead of its current value. +``` + +## Temporal Events + +_Added in [v0.6.6](https://github.com/reflex-dev/reflex/releases/tag/v0.6.6)_ + +### temporal + +The `.temporal` action prevents events from being queued when the backend is down. +This is useful for non-critical events where you do not want them to pile up if there is +a temporary connection issue. + +```md alert warning +# Temporal events are discarded when the backend is down. + +When the backend is unavailable, events with the `.temporal` action will be +discarded rather than queued for later processing. Only use this for events +where it is acceptable to lose some interactions during connection issues. +``` + +In the following example, the `rx.moment` component with `interval` and `on_change` uses `.temporal` to +prevent periodic updates from being queued when the backend is down: + +```python demo exec +class TemporalState(rx.State): + current_time: str = "" + + @rx.event + def update_time(self): + self.current_time = datetime.datetime.now().strftime("%H:%M:%S") + +def temporal_example(): + return rx.vstack( + rx.heading("Current Time:"), + rx.heading(TemporalState.current_time), + rx.moment( + interval=1000, + on_change=TemporalState.update_time.temporal, + ), + rx.text("Time updates will not be queued if the backend is down."), + ) +``` diff --git a/docs/events/event_arguments.md b/docs/events/event_arguments.md new file mode 100644 index 00000000000..42621f5856e --- /dev/null +++ b/docs/events/event_arguments.md @@ -0,0 +1,123 @@ +```python exec +import reflex as rx +``` + +# Event Arguments + +The event handler signature needs to match the event trigger definition argument count. If the event handler takes two arguments, the event trigger must be able to provide two arguments. + +Here is a simple example: + +```python demo exec + +class EventArgStateSlider(rx.State): + value: int = 50 + + @rx.event + def set_end(self, value: list[int | float]): + self.value = value[0] + + +def slider_max_min_step(): + return rx.vstack( + rx.heading(EventArgStateSlider.value), + rx.slider( + default_value=40, + on_value_commit=EventArgStateSlider.set_end, + ), + width="100%", + ) + +``` + +The event trigger here is `on_value_commit` and it is called when the value changes at the end of an interaction. This event trigger passes one argument, which is the value of the slider. The event handler which is triggered by the event trigger must therefore take one argument, which is `value` here. + +Here is a form example: + +```python demo exec + +class EventArgState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def event_arg_example(): + return rx.vstack( + rx.form( + rx.vstack( + rx.input( + placeholder="Name", + name="name", + ), + rx.checkbox("Checked", name="check"), + rx.button("Submit", type="submit"), + ), + on_submit=EventArgState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.heading("Results"), + rx.text(EventArgState.form_data.to_string()), + ) +``` + +In this example the event trigger is the `on_submit` event of the form. The event handler is `handle_submit`. The `on_submit` event trigger passes one argument, the form data as a dictionary, to the `handle_submit` event handler. The `handle_submit` event handler must take one argument because the `on_submit` event trigger passes one argument. + +When the number of args accepted by an EventHandler differs from that provided by the event trigger, an `EventHandlerArgMismatch` error will be raised. + +## Pass Additional Arguments to Event Handlers + +In some use cases, you want to pass additional arguments to your event handlers. To do this you can bind an event trigger to a lambda, which can call your event handler with the arguments you want. + +Try typing a color in an input below and clicking away from it to change the color of the input. + +```python demo exec +class ArgState(rx.State): + colors: list[str] = ["rgba(245,168,152)", "MediumSeaGreen", "#DEADE3"] + + @rx.event + def change_color(self, color: str, index: int): + self.colors[index] = color + +def event_arguments_example(): + return rx.hstack( + rx.input(default_value=ArgState.colors[0], on_blur=lambda c: ArgState.change_color(c, 0), bg=ArgState.colors[0]), + rx.input(default_value=ArgState.colors[1], on_blur=lambda c: ArgState.change_color(c, 1), bg=ArgState.colors[1]), + rx.input(default_value=ArgState.colors[2], on_blur=lambda c: ArgState.change_color(c, 2), bg=ArgState.colors[2]), + ) + +``` + +In this case, in we want to pass two arguments to the event handler `change_color`, the color and the index of the color to change. + +The `on_blur` event trigger passes the text of the input as an argument to the lambda, and the lambda calls the `change_color` event handler with the text and the index of the input. + +When the number of args accepted by a lambda differs from that provided by the event trigger, an `EventFnArgMismatch` error will be raised. + +```md alert warning +# Event Handler Parameters should provide type annotations. + +Like state vars, be sure to provide the right type annotations for the parameters in an event handler. +``` + +## Events with Partial Arguments (Advanced) + +_Added in v0.5.0_ + +Event arguments in Reflex are passed positionally. Any additional arguments not +passed to an EventHandler will be filled in by the event trigger when it is +fired. + +The following two code samples are equivalent: + +```python +# Use a lambda to pass event trigger args to the EventHandler. +rx.text(on_blur=lambda v: MyState.handle_update("field1", v)) + +# Create a partial that passes event trigger args for any args not provided to the EventHandler. +rx.text(on_blur=MyState.handle_update("field1")) +``` diff --git a/docs/events/events_overview.md b/docs/events/events_overview.md new file mode 100644 index 00000000000..fdea0c283f6 --- /dev/null +++ b/docs/events/events_overview.md @@ -0,0 +1,52 @@ +```python exec +import reflex as rx + +from pcweb.pages.docs.library import library +``` + +# Events Overview + +Events are composed of two parts: Event Triggers and Event Handlers. + +- **Events Handlers** are how the State of a Reflex application is updated. They are triggered by user interactions with the UI, such as clicking a button or hovering over an element. Events can also be triggered by the page loading or by other events. + +- **Event triggers** are component props that create an event to be sent to an event handler. + Each component supports a set of events triggers. They are described in each [component's documentation]({library.path}) in the event trigger section. + +## Example + +Lets take a look at an example below. Try mousing over the heading to change the word. + +```python demo exec +class WordCycleState(rx.State): + # The words to cycle through. + text: list[str] = ["Welcome", "to", "Reflex", "!"] + + # The index of the current word. + index: int = 0 + + @rx.event + def next_word(self): + self.index = (self.index + 1) % len(self.text) + + @rx.var + def get_text(self) -> str: + return self.text[self.index] + +def event_triggers_example(): + return rx.heading( + WordCycleState.get_text, + on_mouse_over=WordCycleState.next_word, + color="green", + ) + +``` + +In this example, the heading component has the **event trigger**, `on_mouse_over`. +Whenever the user hovers over the heading, the `next_word` **event handler** will be called to cycle the word. Once the handler returns, the UI will be updated to reflect the new state. + +Adding the `@rx.event` decorator above the event handler is strongly recommended. This decorator enables proper static type checking, which ensures event handlers receive the correct number and types of arguments. + +# What's in this section? + +In the event section of the documentation, you will explore the different types of events supported by Reflex, along with the different ways to call them. diff --git a/docs/events/page_load_events.md b/docs/events/page_load_events.md new file mode 100644 index 00000000000..3c4e9b68678 --- /dev/null +++ b/docs/events/page_load_events.md @@ -0,0 +1,40 @@ +```python exec +import reflex as rx +``` + +# Page Load Events + +You can also specify a function to run when the page loads. This can be useful for fetching data once vs on every render or state change. +In this example, we fetch data when the page loads: + +```python +class State(rx.State): + data: Dict[str, Any] + + @rx.event + def get_data(self): + # Fetch data + self.data = fetch_data() + +@rx.page(on_load=State.get_data) +def index(): + return rx.text('A Beautiful App') +``` + +Another example would be checking if the user is authenticated when the page loads. If the user is not authenticated, we redirect them to the login page. If they are authenticated, we don't do anything, letting them access the page. This `on_load` event would be placed on every page that requires authentication to access. + +```python +class State(rx.State): + authenticated: bool + + @rx.event + def check_auth(self): + # Check if user is authenticated + self.authenticated = check_auth() + if not self.authenticated: + return rx.redirect('/login') + +@rx.page(on_load=State.check_auth) +def index(): + return rx.text('A Beautiful App') +``` diff --git a/docs/events/setters.md b/docs/events/setters.md new file mode 100644 index 00000000000..243926b0e76 --- /dev/null +++ b/docs/events/setters.md @@ -0,0 +1,57 @@ +```python exec +import reflex as rx +``` + +# Setters + +Every base var has a built-in event handler to set it's value for convenience, called `set_VARNAME`. + +Say you wanted to change the value of the select component. You could write your own event handler to do this: + +```python demo exec + +options: list[str] = ["1", "2", "3", "4"] +class SetterState1(rx.State): + selected: str = "1" + + @rx.event + def change(self, value): + self.selected = value + + +def code_setter(): + return rx.vstack( + rx.badge(SetterState1.selected, color_scheme="green"), + rx.select( + options, + on_change= lambda value: SetterState1.change(value), + ) + ) + +``` + +Or you could could use a built-in setter for conciseness. + +```python demo exec + +options: list[str] = ["1", "2", "3", "4"] +class SetterState2(rx.State): + selected: str = "1" + + @rx.event + def set_selected(self, selected: str): + self.selected = selected + +def code_setter_2(): + return rx.vstack( + rx.badge(SetterState2.selected, color_scheme="green"), + rx.select( + options, + on_change= SetterState2.set_selected, + ) + ) +``` + +In this example, the setter for `selected` is `set_selected`. Both of these examples are equivalent. + +Setters are a great way to make your code more concise. But if you want to do something more complicated, you can always write your own function in the state. diff --git a/docs/events/special_events.md b/docs/events/special_events.md new file mode 100644 index 00000000000..0b6ae665e70 --- /dev/null +++ b/docs/events/special_events.md @@ -0,0 +1,28 @@ +```python exec +import reflex as rx + +from pcweb.pages.docs import api_reference +``` + +# Special Events + +Reflex also has built-in special events can be found in the [reference]({api_reference.special_events.path}). + +For example, an event handler can trigger an alert on the browser. + +```python demo exec +class SpecialEventsState(rx.State): + @rx.event + def alert(self): + return rx.window_alert("Hello World!") + +def special_events_example(): + return rx.button("Alert", on_click=SpecialEventsState.alert) +``` + +Special events can also be triggered directly in the UI by attaching them to an event trigger. + +```python +def special_events_example(): + return rx.button("Alert", on_click=rx.window_alert("Hello World!")) +``` diff --git a/docs/events/yield_events.md b/docs/events/yield_events.md new file mode 100644 index 00000000000..78bdb1c02c6 --- /dev/null +++ b/docs/events/yield_events.md @@ -0,0 +1,107 @@ +```python exec +import reflex as rx + +``` + +# Yielding Updates + +A regular event handler will send a `StateUpdate` when it has finished running. This works fine for basic event, but sometimes we need more complex logic. To update the UI multiple times in an event handler, we can `yield` when we want to send an update. + +To do so, we can use the Python keyword `yield`. For every yield inside the function, a `StateUpdate` will be sent to the frontend with the changes up to this point in the execution of the event handler. + +This example below shows how to yield 100 updates to the UI. + +```python demo exec + +class MultiUpdateState(rx.State): + count: int = 0 + + @rx.event + def timed_update(self): + for i in range(100): + self.count += 1 + yield + + +def multi_update(): + return rx.vstack( + rx.text(MultiUpdateState.count), + rx.button("Start", on_click=MultiUpdateState.timed_update) +) + +``` + +Here is another example of yielding multiple updates with a loading icon. + +```python demo exec + +import asyncio + +class ProgressExampleState(rx.State): + count: int = 0 + show_progress: bool = False + + @rx.event + async def increment(self): + self.show_progress = True + yield + # Think really hard. + await asyncio.sleep(0.5) + self.count += 1 + self.show_progress = False + +def progress_example(): + return rx.button( + ProgressExampleState.count, + on_click=ProgressExampleState.increment, + loading=ProgressExampleState.show_progress, + ) + +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=6463&end=6835 +# Video: Asyncio with Yield +``` + +## Yielding Other Events + +Events can also yield other events. This is useful when you want to chain events together. To do this, you can yield the event handler function itself. + +```md alert +# Reference other Event Handler via class + +When chaining another event handler with `yield`, access it via the state class, not `self`. +``` + +```python demo exec + +import asyncio + +class YieldEventsState(rx.State): + count: int = 0 + show_progress: bool = False + + @rx.event + async def add_five(self): + self.show_progress = True + yield + # Think really hard. + await asyncio.sleep(1) + self.count += 5 + self.show_progress = False + + @rx.event + async def increment(self): + yield YieldEventsState.add_five + yield YieldEventsState.add_five + yield YieldEventsState.add_five + + +def multiple_yield_example(): + return rx.button( + YieldEventsState.count, + on_click=YieldEventsState.increment, + loading=YieldEventsState.show_progress, + ) + +``` diff --git a/docs/getting_started/__init__.py b/docs/getting_started/__init__.py new file mode 100644 index 00000000000..0cdc5e99ec8 --- /dev/null +++ b/docs/getting_started/__init__.py @@ -0,0 +1 @@ +"""Getting started docs.""" diff --git a/docs/getting_started/basics.md b/docs/getting_started/basics.md new file mode 100644 index 00000000000..b05d88ce87e --- /dev/null +++ b/docs/getting_started/basics.md @@ -0,0 +1,406 @@ +```python exec +from pcweb.pages.docs import components, getting_started +from pcweb.pages.docs.library import library +from pcweb.pages.docs.custom_components import custom_components +from pcweb.pages import docs +import reflex as rx +``` + +# Reflex Basics + +This page gives an introduction to the most common concepts that you will use to build Reflex apps. + +```md section +# You will learn how to: + +- Create and nest components +- Customize and style components +- Distinguish between compile-time and runtime +- Display data that changes over time +- Respond to events and update the screen +- Render conditions and lists +- Create pages and navigate between them +``` + +[Install]({docs.getting_started.installation.path}) `reflex` using pip. + +```bash +pip install reflex +``` + +Import the `reflex` library to get started. + +```python +import reflex as rx +``` + +## Creating and nesting components + +[Components]({docs.ui.overview.path}) are the building blocks for your app's user interface (UI). They are the visual elements that make up your app, like buttons, text, and images. Reflex has a wide selection of [built-in components]({library.path}) to get you started quickly. + +Components are created using functions that return a component object. + +```python demo exec +def my_button(): + return rx.button("Click Me") +``` + +Components can be nested inside each other to create complex UIs. + +To nest components as children, pass them as positional arguments to the parent component. In the example below, the `rx.text` and `my_button` components are children of the `rx.box` component. + +```python demo exec +def my_page(): + return rx.box( + rx.text("This is a page"), + # Reference components defined in other functions. + my_button() + ) +``` + +You can also use any base HTML element through the [`rx.el`]({docs.library.other.html.path}) namespace. This allows you to use standard HTML elements directly in your Reflex app when you need more control or when a specific component isn't available in the Reflex component library. + +```python demo exec +def my_div(): + return rx.el.div( + rx.el.p("Use base html!"), + ) +``` + +If you need a component not provided by Reflex, you can check the [3rd party ecosystem]({custom_components.path}) or [wrap your own React component]({docs.wrapping_react.library_and_tags.path}). + +## Customizing and styling components + +Components can be customized using [props]({docs.components.props.path}), which are passed in as keyword arguments to the component function. + +Each component has props that are specific to that component. Check the docs for the component you are using to see what props are available. + +```python demo exec +def half_filled_progress(): + return rx.progress(value=50) +``` + +In addition to component-specific props, components can also be styled using CSS properties passed as props. + +```python demo exec +def round_button(): + return rx.button("Click Me", border_radius="15px", font_size="18px") +``` + +```md alert +Use the `snake_case` version of the CSS property name as the prop name. +``` + +See the [styling guide]({docs.styling.overview.path}) for more information on how to style components + +In summary, components are made up of children and props. + +```md definition +# Children + +- Text or other Reflex components nested inside a component. +- Passed as **positional arguments**. + +# Props + +- Attributes that affect the behavior and appearance of a component. +- Passed as **keyword arguments**. +``` + +## Displaying data that changes over time + +Apps need to store and display data that changes over time. Reflex handles this through [State]({docs.state.overview.path}), which is a Python class that stores variables that can change when the app is running, as well as the functions that can change those variables. + +To define a state class, subclass `rx.State` and define fields that store the state of your app. The state variables ([vars]({docs.vars.base_vars.path})) should have a type annotation, and can be initialized with a default value. + +```python +class MyState(rx.State): + count: int = 0 +``` + +### Referencing state vars in components + +To reference a state var in a component, you can pass it as a child or prop. The component will automatically update when the state changes. + +Vars are referenced through class attributes on your state class. For example, to reference the `count` var in a component, use `MyState.count`. + +```python demo exec +class MyState(rx.State): + count: int = 0 + color: str = "red" + +def counter(): + return rx.hstack( + # The heading `color` prop is set to the `color` var in MyState. + rx.heading("Count: ", color=MyState.color), + # The `count` var in `MyState` is passed as a child to the heading component. + rx.heading(MyState.count), + ) +``` + +Vars can be referenced in multiple components, and will automatically update when the state changes. + +## Responding to events and updating the screen + +So far, we've defined state vars but we haven't shown how to change them. All state changes are handled through functions in the state class, called [event handlers]({docs.events.events_overview.path}). + +```md alert +Event handlers are the ONLY way to change state in Reflex. +``` + +Components have special props, such as `on_click`, called event triggers that can be used to make components interactive. Event triggers connect components to event handlers, which update the state. + +```python demo exec +class CounterState(rx.State): + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + +def counter_increment(): + return rx.hstack( + rx.heading(CounterState.count), + rx.button("Increment", on_click=CounterState.increment) + ) +``` + +When an event trigger is activated, the event handler is called, which updates the state. The UI is automatically re-rendered to reflect the new state. + +```md alert info +# What is the `@rx.event` decorator? + +Adding the `@rx.event` decorator above the event handler is strongly recommended. This decorator enables proper static type checking, which ensures event handlers receive the correct number and types of arguments. This was introduced in Reflex version 0.6.5. +``` + +### Event handlers with arguments + +Event handlers can also take in arguments. For example, the `increment` event handler can take an argument to increment the count by a specific amount. + +```python demo exec +class CounterState2(rx.State): + count: int = 0 + + @rx.event + def increment(self, amount: int): + self.count += amount + +def counter_variable(): + return rx.hstack( + rx.heading(CounterState2.count), + rx.button("Increment by 1", on_click=lambda: CounterState2.increment(1)), + rx.button("Increment by 5", on_click=lambda: CounterState2.increment(5)), + ) +``` + +The `on_click` event trigger doesn't pass any arguments here, but some event triggers do. For example, the `on_blur` event trigger passes the text of an input as an argument to the event handler. + +```python demo exec +class TextState(rx.State): + text: str = "" + + @rx.event + def update_text(self, new_text: str): + self.text = new_text + +def text_input(): + return rx.vstack( + rx.heading(TextState.text), + rx.input(default_value=TextState.text, on_blur=TextState.update_text), + ) +``` + +```md alert +Make sure that the event handler has the same number of arguments as the event trigger, or an error will be raised. +``` + +## Compile-time vs. runtime (IMPORTANT) + +Before we dive deeper into state, it's important to understand the difference between compile-time and runtime in Reflex. + +When you run your app, the frontend gets compiled to Javascript code that runs in the browser (compile-time). The backend stays in Python and runs on the server during the lifetime of the app (runtime). + +### When can you not use pure Python? + +We cannot compile arbitrary Python code, only the components that you define. What this means importantly is that you cannot use arbitrary Python operations and functions on state vars in components. + +However, since any event handlers in your state are on the backend, you **can use any Python code or library** within your state. + +### Examples that work + +Within an event handler, use any Python code or library. + +```python demo exec +def check_even(num: int): + return num % 2 == 0 + +class MyState3(rx.State): + count: int = 0 + text: str = "even" + + @rx.event + def increment(self): + # Use any Python code within state. + # Even reference functions defined outside the state. + if check_even(self.count): + self.text = "even" + else: + self.text = "odd" + self.count += 1 + +def count_and_check(): + return rx.box( + rx.heading(MyState3.text), + rx.button("Increment", on_click=MyState3.increment) + ) +``` + +Use any Python function within components, as long as it is defined at compile time (i.e. does not reference any state var) + +```python demo exec +def show_numbers(): + return rx.vstack( + *[ + rx.hstack(i, check_even(i)) + for i in range(10) + ] + ) +``` + +### Examples that don't work + +You cannot do an `if` statement on vars in components, since the value is not known at compile time. + +```python +class BadState(rx.State): + count: int = 0 + +def count_if_even(): + return rx.box( + rx.heading("Count: "), + # This will raise a compile error, as BadState.count is a var and not known at compile time. + rx.text(BadState.count if BadState.count % 2 == 0 else "Odd"), + # Using an if statement with a var as a prop will NOT work either. + rx.text("hello", color="red" if BadState.count % 2 == 0 else "blue"), + ) +``` + +You cannot do a `for` loop over a list of vars. + +```python +class BadState(rx.State): + items: list[str] = ["Apple", "Banana", "Cherry"] + +def loop_over_list(): + return rx.box( + # This will raise a compile error, as BadState.items is a list and not known at compile time. + *[rx.text(item) for item in BadState.items] + ) +``` + +You cannot do arbitrary Python operations on state vars in components. + +```python +class BadTextState(rx.State): + text: str = "Hello world" + +def format_text(): + return rx.box( + # Python operations such as `len` will not work on state vars. + rx.text(len(BadTextState.text)), + ) +``` + +In the next sections, we will show how to handle these cases. + +## Conditional rendering + +As mentioned above, you cannot use Python `if/else` statements with state vars in components. Instead, use the [`rx.cond`]({docs.components.conditional_rendering.path}) function to conditionally render components. + +```python demo exec +class LoginState(rx.State): + logged_in: bool = False + + @rx.event + def toggle_login(self): + self.logged_in = not self.logged_in + +def show_login(): + return rx.box( + rx.cond( + LoginState.logged_in, + rx.heading("Logged In"), + rx.heading("Not Logged In"), + ), + rx.button("Toggle Login", on_click=LoginState.toggle_login) + ) +``` + +## Rendering lists + +To iterate over a var that is a list, use the [`rx.foreach`]({docs.components.rendering_iterables.path}) function to render a list of components. + +Pass the list var and a function that returns a component as arguments to `rx.foreach`. + +```python demo exec +class ListState(rx.State): + items: list[str] = ["Apple", "Banana", "Cherry"] + +def render_item(item: rx.Var[str]): + """Render a single item.""" + # Note that item here is a Var, not a str! + return rx.list.item(item) + +def show_fruits(): + return rx.box( + rx.foreach(ListState.items, render_item), + ) +``` + +The function that renders each item takes in a `Var`, since this will get compiled up front. + +## Var Operations + +You can't use arbitrary Python operations on state vars in components, but Reflex has [var operations]({docs.vars.var_operations.path}) that you can use to manipulate state vars. + +For example, to check if a var is even, you can use the `%` and `==` var operations. + +```python demo exec +class CountEvenState(rx.State): + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + +def count_if_even(): + return rx.box( + rx.heading("Count: "), + rx.cond( + # Here we use the `%` and `==` var operations to check if the count is even. + CountEvenState.count % 2 == 0, + rx.text("Even"), + rx.text("Odd"), + ), + rx.button("Increment", on_click=CountEvenState.increment), + ) +``` + +## App and Pages + +Reflex apps are created by instantiating the `rx.App` class. Pages are linked to specific URL routes, and are created by defining a function that returns a component. + +```python +def index(): + return rx.text('Root Page') + +rx.app = rx.App() +app.add_page(index, route="/") +``` + +## Next Steps + +Now that you have a basic understanding of how Reflex works, the next step is to start coding your own apps. Try one of the following tutorials: + +- [Dashboard Tutorial]({getting_started.dashboard_tutorial.path}) +- [Chatapp Tutorial]({getting_started.chatapp_tutorial.path}) diff --git a/docs/getting_started/chat_tutorial_style.py b/docs/getting_started/chat_tutorial_style.py new file mode 100644 index 00000000000..758a089d987 --- /dev/null +++ b/docs/getting_started/chat_tutorial_style.py @@ -0,0 +1,28 @@ +"""Common styles for questions and answers.""" + +import reflex as rx + +shadow = "rgba(0, 0, 0, 0.15) 0px 2px 8px" +chat_margin = "20%" +message_style = { + "padding": "1em", + "border_radius": "5px", + "margin_y": "0.5em", + "box_shadow": shadow, + "max_width": "30em", + "display": "inline-block", +} + +# Set specific styles for questions and answers. +question_style = message_style | { + "background_color": rx.color("gray", 4), + "margin_left": chat_margin, +} +answer_style = message_style | { + "background_color": rx.color("accent", 8), + "margin_right": chat_margin, +} + +# Styles for the action bar. +input_style = {"border_width": "1px", "box_shadow": shadow, "width": "350px"} +button_style = {"background_color": rx.color("accent", 10), "box_shadow": shadow} diff --git a/docs/getting_started/chat_tutorial_utils.py b/docs/getting_started/chat_tutorial_utils.py new file mode 100644 index 00000000000..4bf4a7d1859 --- /dev/null +++ b/docs/getting_started/chat_tutorial_utils.py @@ -0,0 +1,99 @@ +"""Utility classes for the chat app tutorial.""" + +from __future__ import annotations + +import os + +import openai # pyright: ignore[reportMissingImports] + +import reflex as rx + +OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") + + +class ChatappState(rx.State): + """State for the chat app tutorial.""" + + # The current question being asked. + question: str + + # Keep track of the chat history as a list of (question, answer) tuples. + chat_history: list[tuple[str, str]] + + def set_question(self, q: str): + """Set the current question.""" + self.question = q + + def set_question1(self, q: str): + """Set the current question (variant 1).""" + self.question = q + + def set_question2(self, q: str): + """Set the current question (variant 2).""" + self.question = q + + def set_question3(self, q: str): + """Set the current question (variant 3).""" + self.question = q + + def answer(self) -> None: + """Answer the question with a static response.""" + # Our chatbot is not very smart right now... + answer = "I don't know!" + self.chat_history.append((self.question, answer)) + + def answer2(self) -> None: + """Answer the question and clear the input.""" + # Our chatbot is not very smart right now... + answer = "I don't know!" + self.chat_history.append((self.question, answer)) + # Clear the question input. + self.question = "" + + async def answer3(self): + """Answer with a streaming static response.""" + import asyncio + + # Our chatbot is not very smart right now... + answer = "I don't know!" + self.chat_history.append((self.question, "")) + + # Clear the question input. + self.question = "" + # Yield here to clear the frontend input before continuing. + yield + + for i in range(len(answer)): + await asyncio.sleep(0.1) + self.chat_history[-1] = (self.chat_history[-1][0], answer[: i + 1]) + yield + + async def answer4(self): + """Answer using the OpenAI API with streaming.""" + # Our chatbot has some brains now! + client = openai.AsyncOpenAI(api_key=OPENAI_API_KEY) + session = await client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": self.question}], + stop=None, + temperature=0.7, + stream=True, + ) + + # Add to the answer as the chatbot responds. + answer = "" + self.chat_history.append((self.question, answer)) + + # Clear the question input. + self.question = "" + # Yield here to clear the frontend input before continuing. + yield + + async for item in session: + if hasattr(item.choices[0].delta, "content"): + if item.choices[0].delta.content is None: + # presence of 'None' indicates the end of the response + break + answer += item.choices[0].delta.content + self.chat_history[-1] = (self.chat_history[-1][0], answer) + yield diff --git a/docs/getting_started/chatapp_tutorial.md b/docs/getting_started/chatapp_tutorial.md new file mode 100644 index 00000000000..ebb8ca4d22c --- /dev/null +++ b/docs/getting_started/chatapp_tutorial.md @@ -0,0 +1,791 @@ +```python exec +import os + +import reflex as rx +import openai + +from pcweb.constants import CHAT_APP_URL +from pcweb import constants +from pcweb.pages.docs import components +from pcweb.pages.docs import styling +from pcweb.pages.docs import library +from pcweb.pages.docs import events +from pcweb.pages.docs import state +from pcweb.pages.docs import hosting + +from docs.getting_started import chat_tutorial_style as style +from docs.getting_started.chat_tutorial_utils import ChatappState + +# If it's in environment, no need to hardcode (openai SDK will pick it up) +if "OPENAI_API_KEY" not in os.environ: + openai.api_key = "YOUR_OPENAI_KEY" + +``` + +# Interactive Tutorial: AI Chat App + +This tutorial will walk you through building an AI chat app with Reflex. This app is fairly complex, but don't worry - we'll break it down into small steps. + +You can find the full source code for this app [here]({CHAT_APP_URL}). + +### What You'll Learn + +In this tutorial you'll learn how to: + +1. Install `reflex` and set up your development environment. +2. Create components to define and style your UI. +3. Use state to add interactivity to your app. +4. Deploy your app to share with others. + +## Setting up Your Project + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=175&end=445 +# Video: Example of Setting up the Chat App +``` + +We will start by creating a new project and setting up our development environment. First, create a new directory for your project and navigate to it. + +```bash +~ $ mkdir chatapp +~ $ cd chatapp +``` + +Next, we will create a virtual environment for our project. This is optional, but recommended. In this example, we will use [venv]({constants.VENV_URL}) to create our virtual environment. + +```bash +chatapp $ python3 -m venv venv +$ source venv/bin/activate +``` + +Now, we will install Reflex and create a new project. This will create a new directory structure in our project directory. + +> **Note:** When prompted to select a template, choose option 0 for a blank project. + +```bash +chatapp $ pip install reflex +chatapp $ reflex init +────────────────────────────────── Initializing chatapp ─────────────────────────────────── +Success: Initialized chatapp +chatapp $ ls +assets chatapp rxconfig.py venv +``` + +```python eval +rx.box(height="20px") +``` + +You can run the template app to make sure everything is working. + +```bash +chatapp $ reflex run +─────────────────────────────────── Starting Reflex App ─────────────────────────────────── +Compiling: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00 +─────────────────────────────────────── App Running ─────────────────────────────────────── +App running at: http://localhost:3000 +``` + +```python eval +rx.box(height="20px") +``` + +You should see your app running at [http://localhost:3000]({"http://localhost:3000"}). + +Reflex also starts the backend server which handles all the state management and communication with the frontend. You can test the backend server is running by navigating to [http://localhost:8000/ping]({"http://localhost:8000/ping"}). + +Now that we have our project set up, in the next section we will start building our app! + +## Basic Frontend + +Let's start with defining the frontend for our chat app. In Reflex, the frontend can be broken down into independent, reusable components. See the [components docs]({components.props.path}) for more information. + +### Display A Question And Answer + +We will modify the `index` function in `chatapp/chatapp.py` file to return a component that displays a single question and answer. + +```python demo box +rx.container( + rx.box( + "What is Reflex?", + # The user's question is on the right. + text_align="right", + ), + rx.box( + "A way to build web apps in pure Python!", + # The answer is on the left. + text_align="left", + ), +) +``` + +```python +# chatapp.py + +import reflex as rx + +def index() -> rx.Component: + return rx.container( + rx.box( + "What is Reflex?", + # The user's question is on the right. + text_align="right", + ), + rx.box( + "A way to build web apps in pure Python!", + # The answer is on the left. + text_align="left", + ), + ) + + +# Add state and page to the app. +app = rx.App() +app.add_page(index) +``` + +Components can be nested inside each other to create complex layouts. Here we create a parent container that contains two boxes for the question and answer. + +We also add some basic styling to the components. Components take in keyword arguments, called [props]({components.props.path}), that modify the appearance and functionality of the component. We use the `text_align` prop to align the text to the left and right. + +### Reusing Components + +Now that we have a component that displays a single question and answer, we can reuse it to display multiple questions and answers. We will move the component to a separate function `question_answer` and call it from the `index` function. + +```python exec +def qa(question: str, answer: str) -> rx.Component: + return rx.box( + rx.box(question, text_align="right"), + rx.box(answer, text_align="left"), + margin_y="1em", + ) + + +qa_pairs = [ + ("What is Reflex?", "A way to build web apps in pure Python!"), + ( + "What can I make with it?", + "Anything from a simple website to a complex web app!", + ), +] + + +def chat() -> rx.Component: + qa_pairs = [ + ("What is Reflex?", "A way to build web apps in pure Python!"), + ( + "What can I make with it?", + "Anything from a simple website to a complex web app!", + ), + ] + return rx.box(*[qa(question, answer) for question, answer in qa_pairs]) +``` + +```python demo box +rx.container(chat()) +``` + +```python +def qa(question: str, answer: str) -> rx.Component: + return rx.box( + rx.box(question, text_align="right"), + rx.box(answer, text_align="left"), + margin_y="1em", + ) + + +def chat() -> rx.Component: + qa_pairs = [ + ("What is Reflex?", "A way to build web apps in pure Python!"), + ("What can I make with it?", "Anything from a simple website to a complex web app!"), + ] + return rx.box(*[qa(question, answer) for question, answer in qa_pairs]) + + +def index() -> rx.Component: + return rx.container(chat()) +``` + +### Chat Input + +Now we want a way for the user to input a question. For this, we will use the [input]({library.forms.input.path}) component to have the user add text and a [button]({library.forms.button.path}) component to submit the question. + +```python exec +def action_bar() -> rx.Component: + return rx.hstack( + rx.input(placeholder="Ask a question"), + rx.button("Ask"), + ) +``` + +```python demo box +rx.container( + chat(), + action_bar(), +) +``` + +```python +def action_bar() -> rx.Component: + return rx.hstack( + rx.input(placeholder="Ask a question"), + rx.button("Ask"), + ) + +def index() -> rx.Component: + return rx.container( + chat(), + action_bar(), + ) +``` + +### Styling + +Let's add some styling to the app. More information on styling can be found in the [styling docs]({styling.overview.path}). To keep our code clean, we will move the styling to a separate file `chatapp/style.py`. + +```python +# style.py +import reflex as rx + +# Common styles for questions and answers. +shadow = "rgba(0, 0, 0, 0.15) 0px 2px 8px" +chat_margin = "20%" +message_style = dict( + padding="1em", + border_radius="5px", + margin_y="0.5em", + box_shadow=shadow, + max_width="30em", + display="inline-block", +) + +# Set specific styles for questions and answers. +question_style = message_style | dict(margin_left=chat_margin, background_color=rx.color("gray", 4)) +answer_style = message_style | dict(margin_right=chat_margin, background_color=rx.color("accent", 8)) + +# Styles for the action bar. +input_style = dict( + border_width="1px", padding="0.5em", box_shadow=shadow,width="350px" +) +button_style = dict(background_color=rx.color("accent", 10), box_shadow=shadow) +``` + +We will import the styles in `chatapp.py` and use them in the components. At this point, the app should look like this: + +```python exec +def qa4(question: str, answer: str) -> rx.Component: + return rx.box( + rx.box(rx.text(question, style=style.question_style), text_align="right"), + rx.box(rx.text(answer, style=style.answer_style), text_align="left"), + margin_y="1em", + width="100%", + ) + + +def chat4() -> rx.Component: + qa_pairs = [ + ("What is Reflex?", "A way to build web apps in pure Python!"), + ( + "What can I make with it?", + "Anything from a simple website to a complex web app!", + ), + ] + return rx.box(*[qa4(question, answer) for question, answer in qa_pairs]) + + +def action_bar4() -> rx.Component: + return rx.hstack( + rx.input(placeholder="Ask a question", style=style.input_style), + rx.button("Ask", style=style.button_style), + ) +``` + +```python demo box +rx.center( + rx.vstack( + chat4(), + action_bar4(), + align="center", + ) +) +``` + +```python +# chatapp.py +import reflex as rx + +from chatapp import style + + +def qa(question: str, answer: str) -> rx.Component: + return rx.box( + rx.box(rx.text(question, style=style.question_style), text_align="right"), + rx.box(rx.text(answer, style=style.answer_style), text_align="left"), + margin_y="1em", + width="100%", + ) + +def chat() -> rx.Component: + qa_pairs = [ + ("What is Reflex?", "A way to build web apps in pure Python!"), + ("What can I make with it?", "Anything from a simple website to a complex web app!"), + ] + return rx.box(*[qa(question, answer) for question, answer in qa_pairs]) + + +def action_bar() -> rx.Component: + return rx.hstack( + rx.input(placeholder="Ask a question", style=style.input_style), + rx.button("Ask", style=style.button_style), + ) + + +def index() -> rx.Component: + return rx.center( + rx.vstack( + chat(), + action_bar(), + align="center", + ) + ) + + +app = rx.App() +app.add_page(index) +``` + +The app is looking good, but it's not very useful yet! In the next section, we will add some functionality to the app. + +## State + +Now let’s make the chat app interactive by adding state. The state is where we define all the variables that can change in the app and all the functions that can modify them. You can learn more about state in the [state docs]({state.overview.path}). + +### Defining State + +We will create a new file called `state.py` in the `chatapp` directory. Our state will keep track of the current question being asked and the chat history. We will also define an event handler `answer` which will process the current question and add the answer to the chat history. + +```python +# state.py +import reflex as rx + + +class State(rx.State): + + # The current question being asked. + question: str + + # Keep track of the chat history as a list of (question, answer) tuples. + chat_history: list[tuple[str, str]] + + @rx.event + def answer(self): + # Our chatbot is not very smart right now... + answer = "I don't know!" + self.chat_history.append((self.question, answer)) + +``` + +### Binding State to Components + +Now we can import the state in `chatapp.py` and reference it in our frontend components. We will modify the `chat` component to use the state instead of the current fixed questions and answers. + +```python exec +def qa(question: str, answer: str) -> rx.Component: + return rx.box( + rx.box(rx.text(question, style=style.question_style), text_align="right"), + rx.box(rx.text(answer, style=style.answer_style), text_align="left"), + margin_y="1em", + width="100%", + ) + + +def chat1() -> rx.Component: + return rx.box( + rx.foreach( + ChatappState.chat_history, lambda messages: qa(messages[0], messages[1]) + ) + ) + + +def action_bar1() -> rx.Component: + return rx.hstack( + rx.input( + placeholder="Ask a question", + on_change=ChatappState.set_question, + style=style.input_style, + ), + rx.button("Ask", on_click=ChatappState.answer, style=style.button_style), + ) +``` + +```python demo box +rx.container( + chat1(), + action_bar1(), +) +``` + +```python +# chatapp.py +from chatapp.state import State + + +def chat() -> rx.Component: + return rx.box( + rx.foreach( + State.chat_history, + lambda messages: qa(messages[0], messages[1]) + ) + ) + + + +def action_bar() -> rx.Component: + return rx.hstack( + rx.input(placeholder="Ask a question", on_change=State.set_question1, style=style.input_style), + rx.button("Ask", on_click=State.answer, style=style.button_style), + ) +``` + +Normal Python `for` loops don't work for iterating over state vars because these values can change and aren't known at compile time. Instead, we use the [foreach]({library.dynamic_rendering.foreach.path}) component to iterate over the chat history. + +We also bind the input's `on_change` event to the `set_question` event handler, which will update the `question` state var while the user types in the input. We bind the button's `on_click` event to the `answer` event handler, which will process the question and add the answer to the chat history. The `set_question` event handler is a built-in implicitly defined event handler. Every base var has one. Learn more in the [events docs]({events.setters.path}) under the Setters section. + +### Clearing the Input + +Currently the input doesn't clear after the user clicks the button. We can fix this by binding the value of the input to `question`, with `value=State.question`, and clear it when we run the event handler for `answer`, with `self.question = ''`. + +```python exec +def action_bar2() -> rx.Component: + return rx.hstack( + rx.input( + value=ChatappState.question, + placeholder="Ask a question", + on_change=ChatappState.set_question, + style=style.input_style, + ), + rx.button("Ask", on_click=ChatappState.answer2, style=style.button_style), + ) +``` + +```python demo box +rx.container( + chat1(), + action_bar2(), +) +``` + +```python +# chatapp.py +def action_bar() -> rx.Component: + return rx.hstack( + rx.input( + value=State.question, + placeholder="Ask a question", + on_change=State.set_question2, + style=style.input_style), + rx.button("Ask", on_click=State.answer, style=style.button_style), + ) +``` + +```python +# state.py +@rx.event +def answer(self): + # Our chatbot is not very smart right now... + answer = "I don't know!" + self.chat_history.append((self.question, answer)) + self.question = "" +``` + +### Streaming Text + +Normally state updates are sent to the frontend when an event handler returns. However, we want to stream the text from the chatbot as it is generated. We can do this by yielding from the event handler. See the [yield events docs]({events.yield_events.path}) for more info. + +```python exec +def action_bar3() -> rx.Component: + return rx.hstack( + rx.input( + value=ChatappState.question, + placeholder="Ask a question", + on_change=ChatappState.set_question, + style=style.input_style, + ), + rx.button("Ask", on_click=ChatappState.answer3, style=style.button_style), + ) +``` + +```python demo box +rx.container( + chat1(), + action_bar3(), +) +``` + +```python +# state.py +import asyncio + +async def answer(self): + # Our chatbot is not very smart right now... + answer = "I don't know!" + self.chat_history.append((self.question, "")) + + # Clear the question input. + self.question = "" + # Yield here to clear the frontend input before continuing. + yield + + for i in range(len(answer)): + # Pause to show the streaming effect. + await asyncio.sleep(0.1) + # Add one letter at a time to the output. + self.chat_history[-1] = (self.chat_history[-1][0], answer[:i + 1]) + yield +``` + +In the next section, we will finish our chatbot by adding AI! + +## Final App + +We will use OpenAI's API to give our chatbot some intelligence. + +### Configure the OpenAI API Key + +First, ensure you have an active OpenAI subscription. +Next, install the latest openai package: + +```bash +pip install --upgrade openai +``` + +Direct Configuration of API in Code + +Update the state.py file to include your API key directly: + +```python +# state.py +import os +from openai import AsyncOpenAI + +import reflex as rx + +# Initialize the OpenAI client +client = AsyncOpenAI(api_key="YOUR_OPENAI_API_KEY") # Replace with your actual API key + +``` + +### Using the API + +Making your chatbot intelligent requires connecting to a language model API. This section explains how to integrate with OpenAI's API to power your chatbot's responses. + +1. First, the user types a prompt that is updated via the `on_change` event handler. +2. Next, when a prompt is ready, the user can choose to submit it by clicking the `Ask` button which in turn triggers the `State.answer` method inside our `state.py` file. +3. Finally, if the method is triggered, the `prompt` is sent via a request to OpenAI client and returns an answer that we can trim and use to update the chat history! + +```python +# chatapp.py +def action_bar() -> rx.Component: + return rx.hstack( + rx.input( + value=State.question, + placeholder="Ask a question", + # on_change event updates the input as the user types a prompt. + on_change=State.set_question3, + style=style.input_style), + + # on_click event triggers the API to send the prompt to OpenAI. + rx.button("Ask", on_click=State.answer, style=style.button_style), + ) +``` + +```python +# state.py +import os + +from openai import AsyncOpenAI + +@rx.event +async def answer(self): + # Our chatbot has some brains now! + client = AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"]) + + session = await client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + \{"role": "user", "content": self.question} + ], + stop=None, + temperature=0.7, + stream=True, + ) + + # Add to the answer as the chatbot responds. + answer = "" + self.chat_history.append((self.question, answer)) + + # Clear the question input. + self.question = "" + # Yield here to clear the frontend input before continuing. + yield + + async for item in session: + if hasattr(item.choices[0].delta, "content"): + if item.choices[0].delta.content is None: + # presence of 'None' indicates the end of the response + break + answer += item.choices[0].delta.content + self.chat_history[-1] = (self.chat_history[-1][0], answer) + yield +``` + +Finally, we have our chatbot! + +### Final Code + +This application is a simple, interactive chatbot built with Reflex that leverages OpenAI's API for intelligent responses. The chatbot features a clean interface with streaming responses for a natural conversation experience. + +Key Features + +1. Real-time streaming responses +2. Clean, visually distinct chat bubbles for questions and answers +3. Simple input interface with question field and submit button + +Project Structure + +Below is the full chatbot code with a commented title that corresponds to the filename. + +```text +chatapp/ +├── chatapp.py # UI components and app setup +├── state.py # State management and API integration +└── style.py # Styling definitions +``` + +The `chatapp.py` file: + +```python +import reflex as rx +from chatapp import style +from chatapp.state import State + +def qa(question: str, answer: str) -> rx.Component: + return rx.box( + rx.box(rx.text(question, style=style.question_style), text_align="right"), + rx.box(rx.text(answer, style=style.answer_style), text_align="left"), + margin_y="1em", + ) + +def chat() -> rx.Component: + return rx.box( + rx.foreach( + State.chat_history, + lambda messages: qa(messages[0], messages[1]), + ) + ) + +def action_bar() -> rx.Component: + return rx.hstack( + rx.input( + value=State.question, + placeholder="Ask a question", + on_change=State.set_question, + style=style.input_style, + ), + rx.button( + "Ask", + on_click=State.answer, + style=style.button_style, + ), + ) + +def index() -> rx.Component: + return rx.center( + rx.vstack( + chat(), + action_bar(), + align="center", + ) + ) + +app = rx.App() +app.add_page(index) +``` + +The `state.py` file: + +```python +import os +from openai import AsyncOpenAI +import reflex as rx + +class State(rx.State): + question: str + chat_history: list[tuple[str, str]] = [] + + async def answer(self): + client = AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"]) + + # Start streaming completion from OpenAI + session = await client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + \{"role": "user", "content": self.question} + ], + temperature=0.7, + stream=True, + ) + + # Initialize response and update UI + answer = "" + self.chat_history.append((self.question, answer)) + self.question = "" + yield + + # Process streaming response + async for item in session: + if hasattr(item.choices[0].delta, "content"): + if item.choices[0].delta.content is None: + break + answer += item.choices[0].delta.content + self.chat_history[-1] = (self.chat_history[-1][0], answer) + yield +``` + +The `style.py` file: + +```python +import reflex as rx + +# Common style base +shadow = "rgba(0, 0, 0, 0.15) 0px 2px 8px" +chat_margin = "20%" +message_style = dict( + padding="1em", + border_radius="5px", + margin_y="0.5em", + box_shadow=shadow, + max_width="30em", + display="inline-block", +) + +# Styles for questions and answers +question_style = message_style | dict( + margin_left=chat_margin, + background_color=rx.color("gray", 4), +) +answer_style = message_style | dict( + margin_right=chat_margin, + background_color=rx.color("accent", 8), +) + +# Styles for input elements +input_style = dict(border_width="1px", padding="0.5em", box_shadow=shadow, width="350px") +button_style = dict(background_color=rx.color("accent", 10), box_shadow=shadow) +``` + +### Next Steps + +Congratulations! You have built your first chatbot. From here, you can read through the rest of the documentations to learn about Reflex in more detail. The best way to learn is to build something, so try to build your own app using this as a starting point! + +### One More Thing + +With our hosting service, you can deploy this app with a single command within minutes. Check out our [Hosting Quick Start]({hosting.deploy_quick_start.path}). diff --git a/docs/getting_started/dashboard_tutorial.md b/docs/getting_started/dashboard_tutorial.md new file mode 100644 index 00000000000..77ac09fd1b5 --- /dev/null +++ b/docs/getting_started/dashboard_tutorial.md @@ -0,0 +1,1819 @@ +```python exec +import reflex as rx +from pcweb.pages import docs +``` + +# Tutorial: Data Dashboard + +During this tutorial you will build a small data dashboard, where you can input data and it will be rendered in table and a graph. This tutorial does not assume any existing Reflex knowledge, but we do recommend checking out the quick [Basics Guide]({docs.getting_started.basics.path}) first. + +The techniques you’ll learn in the tutorial are fundamental to building any Reflex app, and fully understanding it will give you a deep understanding of Reflex. + +This tutorial is divided into several sections: + +- **Setup for the Tutorial**: A starting point to follow the tutorial +- **Overview**: The fundamentals of Reflex UI (components and props) +- **Showing Dynamic Data**: How to use State to render data that will change in your app. +- **Add Data to your App**: Using a Form to let a user add data to your app and introduce event handlers. +- **Plotting Data in a Graph**: How to use Reflex's graphing components. +- **Final Cleanup and Conclusion**: How to further customize your app and add some extra styling to it. + +### What are you building? + +In this tutorial, you are building an interactive data dashboard with Reflex. + +You can see what the finished app and code will look like here: + +```python exec +from collections import Counter + +class User(rx.Base): + """The user model.""" + + name: str + email: str + gender: str + +class State5(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + users_for_graph: list[dict] = [] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + self.transform_data() + + return rx.toast.info( + f"User {form_data['name']} has been added.", + position="bottom-right", + ) + + def transform_data(self): + """Transform user gender group data into a format suitable for visualization in graphs.""" + # Count users of each gender group + gender_counts = Counter(user.gender for user in self.users) + + # Transform into list of dict so it can be used in the graph + self.users_for_graph = [ + { + "name": gender_group, + "value": count + } + for gender_group, count in gender_counts.items() + ] + + +def show_user5(user: User): + """Show a user in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + style={"_hover": {"bg": rx.color("gray", 3)}}, + align="center", + ) + +def add_customer_button5() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State5.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) + +def graph5(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=State5.users_for_graph, + width="100%", + height=250, + ) +``` + +```python eval +rx.vstack( + add_customer_button5(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State5.users, show_user5 + ), + ), + variant="surface", + size="3", + width="100%", + ), + graph5(), + align="center", + width="100%", + on_mouse_enter=State5.transform_data, + border_width="2px", + border_radius="10px", + padding="1em", + ) +``` + +```python +import reflex as rx +from collections import Counter + +class User(rx.Base): + """The user model.""" + + name: str + email: str + gender: str + + +class State(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + users_for_graph: list[dict] = [] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + self.transform_data() + + def transform_data(self): + """Transform user gender group data into a format suitable for visualization in graphs.""" + # Count users of each gender group + gender_counts = Counter(user.gender for user in self.users) + + # Transform into list of dict so it can be used in the graph + self.users_for_graph = [ + { + "name": gender_group, + "value": count + } + for gender_group, count in gender_counts.items() + ] + + +def show_user(user: User): + """Show a user in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + style={ + "_hover": { + "bg": rx.color("gray", 3) + } + }, + align="center", + ) + +def add_customer_button() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) + +def graph(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=State.users_for_graph, + width="100%", + height=250, + ) + +def index() -> rx.Component: + return rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", + width="100%", + ), + graph(), + align="center", + width="100%", + ) + + +app = rx.App( + theme=rx.theme( + radius="full", accent_color="grass" + ), +) + +app.add_page( + index, + title="Customer Data App", + description="A simple app to manage customer data.", + on_load=State.transform_data, +) +``` + +Don't worry if you don't understand the code above, in this tutorial we are going to walk you through the whole thing step by step. + +## Setup for the tutorial + +Check out the [installation docs]({docs.getting_started.installation.path}) to get Reflex set up on your machine. Follow these to create a folder called `dashboard_tutorial`, which you will `cd` into and `pip install reflex`. + +We will choose template `0` when we run `reflex init` to get the blank template. Finally run `reflex run` to start the app and confirm everything is set up correctly. + +## Overview + +Now that you’re set up, let’s get an overview of Reflex! + +### Inspecting the starter code + +Within our `dashboard_tutorial` folder we just `cd`'d into, there is a `rxconfig.py` file that contains the configuration for our Reflex app. (Check out the [config docs]({docs.advanced_onboarding.configuration.path}) for more information) + +There is also an `assets` folder where static files such as images and stylesheets can be placed to be referenced within your app. ([asset docs]({docs.assets.overview.path}) for more information) + +Most importantly there is a folder also called `dashboard_tutorial` which contains all the code for your app. Inside of this folder there is a file named `dashboard_tutorial.py`. To begin this tutorial we will delete all the code in this file so that we can start from scratch and explain every step as we go. + +The first thing we need to do is import `reflex`. Once we have done this we can create a component, which is a reusable piece of user interface code. Components are used to render, manage, and update the UI elements in your application. + +Let's look at the example below. Here we have a function called `index` that returns a `text` component (an in-built Reflex UI component) that displays the text "Hello World!". + +Next we define our app using `app = rx.App()` and add the component we just defined (`index`) to a page using `app.add_page(index)`. The function name (in this example `index`) which defines the component, must be what we pass into the `add_page`. The definition of the app and adding a component to a page are required for every Reflex app. + +```python +import reflex as rx + + +def index() -> rx.Component: + return rx.text("Hello World!") + +app = rx.App() +app.add_page(index) +``` + +This code will render a page with the text "Hello World!" when you run your app like below: + +```python eval +rx.text("Hello World!", + border_width="2px", + border_radius="10px", + padding="1em" +) +``` + +```md alert info +For the rest of the tutorial the `app=rx.App()` and `app.add_page` will be implied and not shown in the code snippets. +``` + +### Creating a table + +Let's create a new component that will render a table. We will use the `table` component to do this. The `table` component has a `root`, which takes in a `header` and a `body`, which in turn take in `row` components. The `row` component takes in `cell` components which are the actual data that will be displayed in the table. + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.cell("Danilo Sousa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Male"), + ), + rx.table.row( + rx.table.cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Female"), + ), + ), + border_width="2px", + border_radius="10px", + padding="1em", + ) +``` + +```python +def index() -> rx.Component: + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.cell("Danilo Sousa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Male"), + ), + rx.table.row( + rx.table.cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Female"), + ), + ), + ) +``` + +Components in Reflex have `props`, which can be used to customize the component and are passed in as keyword arguments to the component function. + +The `rx.table.root` component has for example the `variant` and `size` props, which customize the table as seen below. + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.cell("Danilo Sousa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Male"), + ), + rx.table.row( + rx.table.cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Female"), + ), + ), + variant="surface", + size="3", + border_width="2px", + border_radius="10px", + padding="1em", + ) +``` + +```python +def index() -> rx.Component: + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.cell("Danilo Sousa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Male"), + ), + rx.table.row( + rx.table.cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Female"), + ), + ), + variant="surface", + size="3", + ) +``` + +## Showing dynamic data (State) + +Up until this point all the data we are showing in the app is static. This is not very useful for a data dashboard. We need to be able to show dynamic data that can be added to and updated. + +This is where `State` comes in. `State` is a Python class that stores variables that can change when the app is running, as well as the functions that can change those variables. + +To define a state class, subclass `rx.State` and define fields that store the state of your app. The state variables (vars) should have a type annotation, and can be initialized with a default value. Check out the [basics]({docs.getting_started.basics.path}) section for a simple example of how state works. + +In the example below we define a `State` class called `State` that has a variable called `users` that is a list of lists of strings. Each list in the `users` list represents a user and contains their name, email and gender. + +```python +class State(rx.State): + users: list[list[str]] = [ + ["Danilo Sousa", "danilo@example.com", "Male"], + ["Zahra Ambessa", "zahra@example.com", "Female"], + ] +``` + +To iterate over a state var that is a list, we use the [`rx.foreach`]({docs.components.rendering_iterables.path}) function to render a list of components. The `rx.foreach` component takes an `iterable` (list, tuple or dict) and a `function` that renders each item in the `iterable`. + +```md alert info +# Why can we not just splat this in a `for` loop + +You might be wondering why a `foreach` is even needed to render this state variable and why we cannot just splat a `for` loop. Check out this [documentation](<{docs.getting_started.basics.path}#compile-time-vs.-runtime-(important)>) to learn why. +``` + +Here the render function is `show_user` which takes in a single user and returns a `table.row` component that displays the users name, email and gender. + +```python exec +class State1(rx.State): + users: list[list[str]] = [ + ["Danilo Sousa", "danilo@example.com", "Male"], + ["Zahra Ambessa", "zahra@example.com", "Female"], + ] + +def show_user1(person: list): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(person[0]), + rx.table.cell(person[1]), + rx.table.cell(person[2]), + ) +``` + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State1.users, show_user1 + ), + ), + variant="surface", + size="3", + border_width="2px", + border_radius="10px", + padding="1em", +) +``` + +```python +class State(rx.State): + users: list[list[str]] = [ + ["Danilo Sousa", "danilo@example.com", "Male"], + ["Zahra Ambessa", "zahra@example.com", "Female"], + ] + +def show_user(person: list): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(person[0]), + rx.table.cell(person[1]), + rx.table.cell(person[2]), + ) + +def index() -> rx.Component: + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", +) +``` + +As you can see the output above looks the same as before, except now the data is no longer static and can change with user input to the app. + +### Using a proper class structure for our data + +So far our data has been defined in a list of lists, where the data is accessed by index i.e. `user[0]`, `user[1]`. This is not very maintainable as our app gets bigger. + +A better way to structure our data in Reflex is to use a class to represent a user. This way we can access the data using attributes i.e. `user.name`, `user.email`. + +In Reflex when we create these classes to showcase our data, the class must inherit from `rx.Base`. + +`rx.Base` is also necessary if we want to have a state var that is an iterable with different types. For example if we wanted to have `age` as an `int` we would have to use `rx.base` as we could not do this with a state var defined as `list[list[str]]`. + +The `show_user` render function is also updated to access the data by named attributes, instead of indexing. + +```python exec +class User(rx.Base): + """The user model.""" + + name: str + email: str + gender: str + + +class State2(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + +def show_user2(user: User): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + ) +``` + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State2.users, show_user2 + ), + ), + variant="surface", + size="3", + border_width="2px", + border_radius="10px", + padding="1em", +) +``` + +```python +class User(rx.Base): + """The user model.""" + + name: str + email: str + gender: str + + +class State(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + +def show_user(user: User): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + ) + +def index() -> rx.Component: + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", +) +``` + +Next let's add a form to the app so we can add new users to the table. + +## Using a Form to Add Data + +We build a form using `rx.form`, which takes several components such as `rx.input` and `rx.select`, which represent the form fields that allow you to add information to submit with the form. Check out the [form]({docs.library.forms.form.path}) docs for more information on form components. + +The `rx.input` component takes in several props. The `placeholder` prop is the text that is displayed in the input field when it is empty. The `name` prop is the name of the input field, which gets passed through in the dictionary when the form is submitted. The `required` prop is a boolean that determines if the input field is required. + +The `rx.select` component takes in a list of options that are displayed in the dropdown. The other props used here are identical to the `rx.input` component. + +```python demo +rx.form( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), +) +``` + +This form is all very compact as you can see from the example, so we need to add some styling to make it look better. We can do this by adding a `vstack` component around the form fields. The `vstack` component stacks the form fields vertically. Check out the [layout]({docs.styling.layout.path}) docs for more information on how to layout your app. + +```python demo +rx.form( + rx.vstack( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + ), +) +``` + +Now you have probably realised that we have all the form fields, but we have no way to submit the form. We can add a submit button to the form by adding a `rx.button` component to the `vstack` component. The `rx.button` component takes in the text that is displayed on the button and the `type` prop which is the type of button. The `type` prop is set to `submit` so that the form is submitted when the button is clicked. + +In addition to this we need a way to update the `users` state variable when the form is submitted. All state changes are handled through functions in the state class, called [event handlers]({docs.events.events_overview.path}). + +Components have special props called event triggers, such as `on_submit`, that can be used to make components interactive. Event triggers connect components to event handlers, which update the state. Different event triggers expect the event handler that you hook them up to, to take in different arguments (and some do not take in any arguments). + +The `on_submit` event trigger of `rx.form` is hooked up to the `add_user` event handler that is defined in the `State` class. This event trigger expects to pass a `dict`, containing the form data, to the event handler that it is hooked up to. The `add_user` event handler takes in the form data as a dictionary and appends it to the `users` state variable. + +```python +class State(rx.State): + + ... + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + + +def form(): + return rx.form( + rx.vstack( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.button("Submit", type="submit"), + ), + on_submit=State.add_user, + reset_on_submit=True, + ) +``` + +Finally we must add the new `form()` component we have defined to the `index()` function so that the form is rendered on the page. + +Below is the full code for the app so far. If you try this form out you will see that you can add new users to the table by filling out the form and clicking the submit button. The form data will also appear as a toast (a small window in the corner of the page) on the screen when submitted. + +```python exec +class State3(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + + + return rx.toast.info( + f"User has been added: {form_data}.", + position="bottom-right", + ) + +def show_user(user: User): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + ) + +def form(): + return rx.form( + rx.vstack( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.button("Submit", type="submit"), + ), + on_submit=State3.add_user, + reset_on_submit=True, + ) +``` + +```python eval +rx.vstack( + form(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State3.users, show_user + ), + ), + variant="surface", + size="3", + ), + border_width="2px", + border_radius="10px", + padding="1em", +) +``` + +```python +class State(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + + +def show_user(user: User): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + ) + +def form(): + return rx.form( + rx.vstack( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.button("Submit", type="submit"), + ), + on_submit=State.add_user, + reset_on_submit=True, + ) + +def index() -> rx.Component: + return rx.vstack( + form(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", + ), + ) +``` + +### Putting the Form in an Overlay + +In Reflex, we like to make the user interaction as intuitive as possible. Placing the form we just constructed in an overlay creates a focused interaction by dimming the background, and ensures a cleaner layout when you have multiple action points such as editing and deleting as well. + +We will place the form inside of a `rx.dialog` component (also called a modal). The `rx.dialog.root` contains all the parts of a dialog, and the `rx.dialog.trigger` wraps the control that will open the dialog. In our case the trigger will be an `rx.button` that says "Add User" as shown below. + +```python +rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), +) +``` + +After the trigger we have the `rx.dialog.content` which contains everything within our dialog, including a title, a description and our form. The first way to close the dialog is without submitting the form and the second way is to close the dialog by submitting the form as shown below. This requires two `rx.dialog.close` components within the dialog. + +```python +rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), +), +rx.dialog.close( + rx.button( + "Submit", type="submit" + ), +) +``` + +The total code for the dialog with the form in it is below. + +```python demo +rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + # flex is similar to vstack and used to layout the form fields + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State3.add_user, + reset_on_submit=False, + ), + # max_width is used to limit the width of the dialog + max_width="450px", + ), +) +``` + +At this point we have an app that allows you to add users to a table by filling out a form. The form is placed in a dialog that can be opened by clicking the "Add User" button. We change the name of the component from `form` to `add_customer_button` and update this in our `index` component. The full app so far and code are below. + +```python exec +def add_customer_button() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State3.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) +``` + +```python eval +rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State3.users, show_user + ), + ), + variant="surface", + size="3", + ), + border_width="2px", + border_radius="10px", + padding="1em", +) +``` + +```python +class User(rx.Base): + """The user model.""" + + name: str + email: str + gender: str + + +class State(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + + + +def show_user(user: User): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + ) + +def add_customer_button() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) + +def index() -> rx.Component: + return rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", + ), + ) +``` + +## Plotting Data in a Graph + +The last part of this tutorial is to plot the user data in a graph. We will use Reflex's built-in graphing library recharts to plot the number of users of each gender. + +### Transforming the data for the graph + +The graphing components in Reflex expect to take in a list of dictionaries. Each dictionary represents a data point on the graph and contains the x and y values. We will create a new event handler in the state called `transform_data` to transform the user data into the format that the graphing components expect. We must also create a new state variable called `users_for_graph` to store the transformed data, which will be used to render the graph. + +```python +from collections import Counter + +class State(rx.State): + users: list[User] = [] + users_for_graph: list[dict] = [] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + self.transform_data() + + def transform_data(self): + """Transform user gender group data into a format suitable for visualization in graphs.""" + # Count users of each gender group + gender_counts = Counter(user.gender for user in self.users) + + # Transform into list of dict so it can be used in the graph + self.users_for_graph = [ + { + "name": gender_group, + "value": count + } + for gender_group, count in gender_counts.items() + ] +``` + +As we can see above the `transform_data` event handler uses the `Counter` class from the `collections` module to count the number of users of each gender. We then create a list of dictionaries from this which we set to the state var `users_for_graph`. + +Finally we can see that whenever we add a new user through submitting the form and running the `add_user` event handler, we call the `transform_data` event handler to update the `users_for_graph` state variable. + +### Rendering the graph + +We use the `rx.recharts.bar_chart` component to render the graph. We pass through the state variable for our graphing data as `data=State.users_for_graph`. We also pass in a `rx.recharts.bar` component which represents the bars on the graph. The `rx.recharts.bar` component takes in the `data_key` prop which is the key in the data dictionary that represents the y value of the bar. The `stroke` and `fill` props are used to set the color of the bars. + +The `rx.recharts.bar_chart` component also takes in `rx.recharts.x_axis` and `rx.recharts.y_axis` components which represent the x and y axes of the graph. The `data_key` prop of the `rx.recharts.x_axis` component is set to the key in the data dictionary that represents the x value of the bar. Finally we add `width` and `height` props to set the size of the graph. + +```python +def graph(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=State.users_for_graph, + width="100%", + height=250, + ) +``` + +Finally we add this `graph()` component to our `index()` component so that the graph is rendered on the page. The code for the full app with the graph included is below. If you try this out you will see that the graph updates whenever you add a new user to the table. + +```python exec +from collections import Counter + +class State4(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + users_for_graph: list[dict] = [] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + self.transform_data() + + return rx.toast.info( + f"User {form_data['name']} has been added.", + position="bottom-right", + ) + + def transform_data(self): + """Transform user gender group data into a format suitable for visualization in graphs.""" + # Count users of each gender group + gender_counts = Counter(user.gender for user in self.users) + + # Transform into list of dict so it can be used in the graph + self.users_for_graph = [ + { + "name": gender_group, + "value": count + } + for gender_group, count in gender_counts.items() + ] + +def add_customer_button() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="Male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State4.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) + +def graph(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=State4.users_for_graph, + width="100%", + height=250, + ) +``` + +```python eval +rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State4.users, show_user + ), + ), + variant="surface", + size="3", + ), + graph(), + border_width="2px", + border_radius="10px", + padding="1em", +) +``` + +```python +from collections import Counter + +class State(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + users_for_graph: list[dict] = [] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + self.transform_data() + + def transform_data(self): + """Transform user gender group data into a format suitable for visualization in graphs.""" + # Count users of each gender group + gender_counts = Counter(user.gender for user in self.users) + + # Transform into list of dict so it can be used in the graph + self.users_for_graph = [ + { + "name": gender_group, + "value": count + } + for gender_group, count in gender_counts.items() + ] + + +def show_user(user: User): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + ) + +def add_customer_button() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) + +def graph(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=State.users_for_graph, + width="100%", + height=250, + ) + +def index() -> rx.Component: + return rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", + ), + graph(), + ) +``` + +One thing you may have noticed about your app is that the graph does not appear initially when you run the app, and that you must add a user to the table for it to first appear. This occurs because the `transform_data` event handler is only called when a user is added to the table. In the next section we will explore a solution to this. + +## Final Cleanup + +### Revisiting app.add_page + +At the beginning of this tutorial we mentioned that the `app.add_page` function is required for every Reflex app. This function is used to add a component to a page. + +The `app.add_page` currently looks like this `app.add_page(index)`. We could change the route that the page renders on by setting the `route` prop such as `route="/custom-route"`, this would change the route to `http://localhost:3000/custom-route` for this page. + +We can also set a `title` to be shown in the browser tab and a `description` as shown in search results. + +To solve the problem we had above about our graph not loading when the page loads, we can use `on_load` inside of `app.add_page` to call the `transform_data` event handler when the page loads. This would look like `on_load=State.transform_data`. Below see what our `app.add_page` would look like with some of the changes above added. + +```python eval +rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State4.users, show_user + ), + ), + variant="surface", + size="3", + ), + graph(), + on_mouse_enter=State4.transform_data, + border_width="2px", + border_radius="10px", + padding="1em", +) +``` + +```python +app.add_page( + index, + title="Customer Data App", + description="A simple app to manage customer data.", + on_load=State.transform_data, +) +``` + +### Revisiting app=rx.App() + +At the beginning of the tutorial we also mentioned that we defined our app using `app=rx.App()`. We can also pass in some props to the `rx.App` component to customize the app. + +The most important one is `theme` which allows you to customize the look and feel of the app. The `theme` prop takes in an `rx.theme` component which has several props that can be set. + +The `radius` prop sets the global radius value for the app that is inherited by all components that have a `radius` prop. It can be overwritten locally for a specific component by manually setting the `radius` prop. + +The `accent_color` prop sets the accent color of the app. Check out other options for the accent color [here]({docs.library.other.theme.path}). + +To see other props that can be set at the app level check out this [documentation]({docs.styling.theming.path}) + +```python +app = rx.App( + theme=rx.theme( + radius="full", accent_color="grass" + ), +) +``` + +Unfortunately in this tutorial here we cannot actually apply this to the live example on the page, but if you copy and paste the code below into a reflex app locally you can see it in action. + +## Conclusion + +Finally let's make some final styling updates to our app. We will add some hover styling to the table rows and center the table inside the `show_user` with `style=\{"_hover": \{"bg": rx.color("gray", 3)}}, align="center"`. + +In addition, we will add some `width="100%"` and `align="center"` to the `index()` component to center the items on the page and ensure they stretch the full width of the page. + +Check out the full code and interactive app below: + +```python eval +rx.vstack( + add_customer_button5(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State5.users, show_user5 + ), + ), + variant="surface", + size="3", + width="100%", + ), + graph5(), + align="center", + width="100%", + on_mouse_enter=State5.transform_data, + border_width="2px", + border_radius="10px", + padding="1em", + ) +``` + +```python +import reflex as rx +from collections import Counter + +class User(rx.Base): + """The user model.""" + + name: str + email: str + gender: str + + +class State(rx.State): + users: list[User] = [ + User(name="Danilo Sousa", email="danilo@example.com", gender="Male"), + User(name="Zahra Ambessa", email="zahra@example.com", gender="Female"), + ] + users_for_graph: list[dict] = [] + + def add_user(self, form_data: dict): + self.users.append(User(**form_data)) + self.transform_data() + + def transform_data(self): + """Transform user gender group data into a format suitable for visualization in graphs.""" + # Count users of each gender group + gender_counts = Counter(user.gender for user in self.users) + + # Transform into list of dict so it can be used in the graph + self.users_for_graph = [ + { + "name": gender_group, + "value": count + } + for gender_group, count in gender_counts.items() + ] + + +def show_user(user: User): + """Show a user in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.gender), + style={ + "_hover": { + "bg": rx.color("gray", 3) + } + }, + align="center", + ) + +def add_customer_button() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name", required=True + ), + rx.input( + placeholder="user@reflex.dev", + name="email", + ), + rx.select( + ["Male", "Female"], + placeholder="male", + name="gender", + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button( + "Submit", type="submit" + ), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State.add_user, + reset_on_submit=False, + ), + max_width="450px", + ), + ) + +def graph(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=State.users_for_graph, + width="100%", + height=250, + ) + +def index() -> rx.Component: + return rx.vstack( + add_customer_button(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Gender"), + ), + ), + rx.table.body( + rx.foreach( + State.users, show_user + ), + ), + variant="surface", + size="3", + width="100%", + ), + graph(), + align="center", + width="100%", + ) + + +app = rx.App( + theme=rx.theme( + radius="full", accent_color="grass" + ), +) + +app.add_page( + index, + title="Customer Data App", + description="A simple app to manage customer data.", + on_load=State.transform_data, +) +``` + +And that is it for your first dashboard tutorial. In this tutorial we have created + +- a table to display user data +- a form to add new users to the table +- a dialog to showcase the form +- a graph to visualize the user data + +In addition to the above we have we have + +- explored state to allow you to show dynamic data that changes over time +- explored events to allow you to make your app interactive and respond to user actions +- added styling to the app to make it look better + +## Advanced Section (Hooking this up to a Database) + +Coming Soon! diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md new file mode 100644 index 00000000000..edb63660d60 --- /dev/null +++ b/docs/getting_started/installation.md @@ -0,0 +1,168 @@ +```python exec +from pcweb import constants +import reflex as rx +from pcweb.pages.gallery import gallery +app_name = "my_app_name" +default_url = "http://localhost:3000" +``` + +# Installation + +Reflex requires Python 3.10+. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=758&end=1206 +# Video: Installation +``` + +## Virtual Environment + +We **highly recommend** creating a virtual environment for your project. + +[uv]({constants.UV_URL}) is the recommended modern option. [venv]({constants.VENV_URL}), [conda]({constants.CONDA_URL}) and [poetry]({constants.POETRY_URL}) are some alternatives. + +# Install Reflex on your system + +---md tabs + +--tab macOS/Linux + +## Install on macOS/Linux + +We will go with [uv]({constants.UV_URL}) here. + +### Prerequisites + +#### Install uv + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +After installation, restart your terminal or run `source ~/.bashrc` (or `source ~/.zshrc` for zsh). + +Alternatively, install via [Homebrew, PyPI, or other methods](https://docs.astral.sh/uv/getting-started/installation/). + +**macOS (Apple Silicon) users:** Install [Rosetta 2](https://support.apple.com/en-us/HT211861). Run this command: + +`/usr/sbin/softwareupdate --install-rosetta --agree-to-license` + +### Create the project directory + +Replace `{app_name}` with your project name. Switch to the new directory. + +```bash +mkdir {app_name} +cd {app_name} +``` + +### Initialize uv project + +```bash +uv init +``` + +### Add Reflex to the project + +```bash +uv add reflex +``` + +### Initialize the Reflex project + +```bash +uv run reflex init +``` + +-- +--tab Windows + +## Install on Windows + +For Windows users, we recommend using [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/about) for optimal performance. + +**WSL users:** Refer to the macOS/Linux instructions above. + +For the rest of this section we will work with native Windows (non-WSL). + +We will go with [uv]({constants.UV_URL}) here. + +### Prerequisites + +#### Install uv + +```powershell +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +After installation, restart your terminal (PowerShell or Command Prompt). + +Alternatively, install via [WinGet, Scoop, or other methods](https://docs.astral.sh/uv/getting-started/installation/). + +### Create the project directory + +Replace `{app_name}` with your project name. Switch to the new directory. + +```bash +mkdir {app_name} +cd {app_name} +``` + +### Initialize uv project + +```bash +uv init +``` + +### Add Reflex to the project + +```bash +uv add reflex +``` + +### Initialize the Reflex project + +```bash +uv run reflex init +``` + +```md alert warning +# Error `Install Failed - You are missing a DLL required to run bun.exe` Windows + +Bun requires runtime components of Visual C++ libraries to run on Windows. This issue is fixed by installing [Microsoft Visual C++ 2015 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=53840). +``` + +-- + +--- + +Running `uv run reflex init` will return the option to start with a blank Reflex app, premade templates built by the Reflex team, or to try our [AI builder]({constants.REFLEX_BUILD_URL}). + +```bash +Initializing the web directory. + +Get started with a template: +(0) A blank Reflex app. +(1) Premade templates built by the Reflex team. +(2) Try our AI builder. +Which template would you like to use? (0): +``` + +From here select an option. + +## Run the App + +Run it in development mode: + +```bash +uv run reflex run +``` + +Your app runs at [http://localhost:3000](http://localhost:3000). + +Reflex prints logs to the terminal. To increase log verbosity to help with debugging, use the `--loglevel` flag: + +```bash +uv run reflex run --loglevel debug +``` + +Reflex will _hot reload_ any code changes in real time when running in development mode. Your code edits will show up on [http://localhost:3000](http://localhost:3000) automatically. diff --git a/docs/getting_started/introduction.md b/docs/getting_started/introduction.md new file mode 100644 index 00000000000..3e14c84dee3 --- /dev/null +++ b/docs/getting_started/introduction.md @@ -0,0 +1,353 @@ +```python exec +import reflex as rx +from pcweb import constants, styles +from pcweb.pages.docs import getting_started +from pcweb.pages.docs import wrapping_react +from pcweb.pages.docs.library import library +from pcweb.pages.docs import pages +from pcweb.pages.docs import vars +from pcweb.styles.colors import c_color +from pcweb.pages.docs import styling +from pcweb.styles.fonts import base +from pcweb.pages.docs import hosting +from pcweb.flexdown import markdown_with_shiki +from pcweb.pages.docs import advanced_onboarding +``` + + + +# Introduction + +**Reflex** is an open-source framework for quickly building beautiful, interactive web applications in **pure Python**. + +## Goals + +```md section +### Pure Python + +Use Python for everything. Don't worry about learning a new language. + +### Easy to Learn + +Build and share your first app in minutes. No web development experience required. + +### Full Flexibility + +Remain as flexible as traditional web frameworks. Reflex is easy to use, yet allows for advanced use cases. + +Build anything from small data science apps to large, multi-page websites. **This entire site was built and deployed with Reflex!** + +### Batteries Included + +No need to reach for a bunch of different tools. Reflex handles the user interface, server-side logic, and deployment of your app. +``` + +## An example: Make it count + +Here, we go over a simple counter app that lets the user count up or down. + +```python exec +class CounterExampleState(rx.State): + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + + @rx.event + def decrement(self): + self.count -= 1 + +class IntroTabsState(rx.State): + """The app state.""" + + value: str = "tab1" + tab_selected: str = "" + + @rx.event + def change_value(self, val: str): + self.tab_selected = f"{val} clicked!" + self.value = val + +def tabs(): + return rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger( + "Frontend", value="tab1", + class_name="tab-style" + ), + rx.tabs.trigger( + "Backend", value="tab2", + class_name="tab-style" + ), + rx.tabs.trigger( + "Page", value="tab3", + class_name="tab-style" + ), + ), + rx.tabs.content( + markdown_with_shiki( + """The frontend is built declaratively using Reflex components. Components are compiled down to JS and served to the users browser, therefore: + +- Only use Reflex components, vars, and var operations when building your UI. Any other logic should be put in your `State` (backend). + +- Use `rx.cond` and `rx.foreach` (replaces if statements and for loops), for creating dynamic UIs. + """, + ), + value="tab1", + class_name="pt-4" + ), + rx.tabs.content( + markdown_with_shiki( + """Write your backend in the `State` class. Here you can define functions and variables that can be referenced in the frontend. This code runs directly on the server and is not compiled, so there are no special caveats. Here you can use any Python external library and call any method/function. + """, + ), + value="tab2", + class_name="pt-4" + ), + rx.tabs.content( + markdown_with_shiki( + f"""Each page is a Python function that returns a Reflex component. You can define multiple pages and navigate between them, see the [Routing]({pages.overview.path}) section for more information. + +- Start with a single page and scale to 100s of pages. + """, + ), + value="tab3", + class_name="pt-4" + ), + class_name="text-slate-12 font-normal", + default_value="tab1", + value=IntroTabsState.value, + on_change=lambda x: IntroTabsState.change_value( + x + ), + ) +``` + +```python demo box id=counter +rx.hstack( + rx.button( + "Decrement", + color_scheme="ruby", + on_click=CounterExampleState.decrement, + ), + rx.heading(CounterExampleState.count, font_size="2em"), + rx.button( + "Increment", + color_scheme="grass", + on_click=CounterExampleState.increment, + ), + spacing="4", +) +``` + +Here is the full code for this example: + +```python eval +tabs() +``` + +```python demo box +rx.box( + rx._x.code_block( + """import reflex as rx """, + class_name="code-block !bg-transparent !border-none", + ), + rx._x.code_block( + """class State(rx.State): + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + + @rx.event + def decrement(self): + self.count -= 1""", + background=rx.cond( + IntroTabsState.value == "tab2", + "var(--c-violet-3) !important", + "transparent", + ), + border=rx.cond( + IntroTabsState.value == "tab2", + "1px solid var(--c-violet-5)", + "none !important" + ), + class_name="code-block", + ), + rx._x.code_block( + """def index(): + return rx.hstack( + rx.button( + "Decrement", + color_scheme="ruby", + on_click=State.decrement, + ), + rx.heading(State.count, font_size="2em"), + rx.button( + "Increment", + color_scheme="grass", + on_click=State.increment, + ), + spacing="4", + )""", + border=rx.cond( + IntroTabsState.value == "tab1", + "1px solid var(--c-violet-5)", + "none !important", + ), + background=rx.cond( + IntroTabsState.value == "tab1", + "var(--c-violet-3) !important", + "transparent", + ), + class_name="code-block", + ), + rx._x.code_block( + """app = rx.App() +app.add_page(index)""", + background=rx.cond( + IntroTabsState.value == "tab3", + "var(--c-violet-3) !important", + "transparent", + ), + border=rx.cond( + IntroTabsState.value == "tab3", + "1px solid var(--c-violet-5)", + "none !important", + ), + class_name="code-block", + ), + class_name="w-full flex flex-col", +) +``` + +## The Structure of a Reflex App + +Let's break this example down. + +### Import + +```python +import reflex as rx +``` + +We begin by importing the `reflex` package (aliased to `rx`). We reference Reflex objects as `rx.*` by convention. + +### State + +```python +class State(rx.State): + count: int = 0 +``` + +The state defines all the variables (called **[vars]({vars.base_vars.path})**) in an app that can change, as well as the functions (called **[event_handlers](#event-handlers)**) that change them. + +Here our state has a single var, `count`, which holds the current value of the counter. We initialize it to `0`. + +### Event Handlers + +```python +@rx.event +def increment(self): + self.count += 1 + +@rx.event +def decrement(self): + self.count -= 1 +``` + +Within the state, we define functions, called **event handlers**, that change the state vars. + +Event handlers are the only way that we can modify the state in Reflex. +They can be called in response to user actions, such as clicking a button or typing in a text box. +These actions are called **events**. + +Our counter app has two event handlers, `increment` and `decrement`. + +### User Interface (UI) + +```python +def index(): + return rx.hstack( + rx.button( + "Decrement", + color_scheme="ruby", + on_click=State.decrement, + ), + rx.heading(State.count, font_size="2em"), + rx.button( + "Increment", + color_scheme="grass", + on_click=State.increment, + ), + spacing="4", + ) +``` + +This function defines the app's user interface. + +We use different components such as `rx.hstack`, `rx.button`, and `rx.heading` to build the frontend. Components can be nested to create complex layouts, and can be styled using the full power of CSS or [Tailwind CSS]({styling.tailwind.path}). + +Reflex comes with [50+ built-in components]({library.path}) to help you get started. +We are actively adding more components. Also, it's easy to [wrap your own React components]({wrapping_react.overview.path}). + +```python +rx.heading(State.count, font_size="2em"), +``` + +Components can reference the app's state vars. +The `rx.heading` component displays the current value of the counter by referencing `State.count`. +All components that reference state will reactively update whenever the state changes. + +```python +rx.button( + "Decrement", + color_scheme="ruby", + on_click=State.decrement, +), +``` + +Components interact with the state by binding events triggers to event handlers. +For example, `on_click` is an event that is triggered when a user clicks a component. + +The first button in our app binds its `on_click` event to the `State.decrement` event handler. Similarly the second button binds `on_click` to `State.increment`. + +In other words, the sequence goes like this: + +- User clicks "increment" on the UI. +- `on_click` event is triggered. +- Event handler `State.increment` is called. +- `State.count` is incremented. +- UI updates to reflect the new value of `State.count`. + +### Add pages + +Next we define our app and add the counter component to the base route. + +```python +app = rx.App() +app.add_page(index) +``` + +## Next Steps + +🎉 And that's it! + +We've created a simple, yet fully interactive web app in pure Python. + +By continuing with our documentation, you will learn how to build awesome apps with Reflex. Use the sidebar to navigate through the sections, or search (`Ctrl+K` or `Cmd+K`) to quickly find a page. + +For a glimpse of the possibilities, check out these resources: + +- For a more real-world example, check out either the [dashboard tutorial]({getting_started.dashboard_tutorial.path}) or the [chatapp tutorial]({getting_started.chatapp_tutorial.path}). +- Check out our open-source [templates]({getting_started.open_source_templates.path})! +- We have an AI Builder that can generate full Reflex apps or help with your existing app! Check it out at [Reflex Build]({constants.REFLEX_BUILD_URL})! +- Deploy your app with a single command using [Reflex Cloud]({hosting.deploy_quick_start.path})! + +If you want to learn more about how Reflex works, check out the [How Reflex Works]({advanced_onboarding.how_reflex_works.path}) section. + +## Join our Community + +If you have questions about anything related to Reflex, you're always welcome to ask our community on [GitHub Discussions]({constants.GITHUB_DISCUSSIONS_URL}), [Discord]({constants.DISCORD_URL}), [Forum]({constants.FORUM_URL}), and [X]({constants.TWITTER_URL}). diff --git a/docs/getting_started/open_source_templates.md b/docs/getting_started/open_source_templates.md new file mode 100644 index 00000000000..aa28a5e23e7 --- /dev/null +++ b/docs/getting_started/open_source_templates.md @@ -0,0 +1,67 @@ +# Open Source Templates + +Check out what the community is building with Reflex. See 2000+ more public projects on [Github](https://github.com/reflex-dev/reflex/network/dependents). Want to get your app featured? Submit it [here](https://github.com/reflex-dev/templates). Copy the template command and use it during `reflex init` + +```python exec + +import reflex as rx + +from pcweb.components.code_card import gallery_app_card +from pcweb.components.webpage.comps import h1_title +from pcweb.pages.gallery.sidebar import TemplatesState, pagination, sidebar +from pcweb.templates.webpage import webpage + + +@rx.memo +def skeleton_card() -> rx.Component: + return rx.skeleton( + class_name="box-border shadow-large border rounded-xl w-full h-[280px] overflow-hidden", + loading=True, + ) + + +def component_grid() -> rx.Component: + from pcweb.pages.gallery.apps import gallery_apps_data + + posts = [] + for path, document in list(gallery_apps_data.items()): + posts.append( + rx.cond( + TemplatesState.filtered_templates.contains(document.metadata["title"]), + gallery_app_card(app=document.metadata), + None, + ) + ) + return rx.box( + *posts, + rx.box( + rx.el.h4( + "No templates found", + class_name="text-base font-semibold text-slate-12 text-nowrap", + ), + class_name="flex-col gap-2 flex absolute left-1 top-0 z-[-1] w-full", + ), + class_name="gap-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 w-full relative", + ) + + +def gallery() -> rx.Component: + return rx.el.section( + rx.box( + sidebar(), + rx.box( + component_grid(), + pagination(), + class_name="flex flex-col", + ), + class_name="flex flex-col gap-6 lg:gap-10 w-full", + ), + id="gallery", + class_name="mx-auto", + ) + +``` + +```python eval +gallery() +``` diff --git a/docs/getting_started/project-structure.md b/docs/getting_started/project-structure.md new file mode 100644 index 00000000000..b47e278f568 --- /dev/null +++ b/docs/getting_started/project-structure.md @@ -0,0 +1,73 @@ +# Project Structure + +```python exec +from pcweb.pages.docs import advanced_onboarding +from pcweb.constants import REFLEX_ASSETS_CDN +``` + +## Directory Structure + +```python exec +app_name = "hello" +``` + +Let's create a new app called `{app_name}` + +```bash +mkdir {app_name} +cd {app_name} +reflex init +``` + +This will create a directory structure like this: + +```bash +{app_name} +├── .web +├── assets +├── {app_name} +│ ├── __init__.py +│ └── {app_name}.py +└── rxconfig.py +``` + +Let's go over each of these directories and files. + +## .web + +This is where the compiled Javascript files will be stored. You will never need to touch this directory, but it can be useful for debugging. + +Each Reflex page will compile to a corresponding `.js` file in the `.web/pages` directory. + +## Assets + +The `assets` directory is where you can store any static assets you want to be publicly available. This includes images, fonts, and other files. + +For example, if you save an image to `assets/image.png` you can display it from your app like this: + +```python +rx.image(src=f"{REFLEX_ASSETS_CDN}other/image.png") +``` + +j + +## Main Project + +Initializing your project creates a directory with the same name as your app. This is where you will write your app's logic. + +Reflex generates a default app within the `{app_name}/{app_name}.py` file. You can modify this file to customize your app. + +## Configuration + +The `rxconfig.py` file can be used to configure your app. By default it looks something like this: + +```python +import reflex as rx + + +config = rx.Config( + app_name="{app_name}", +) +``` + +We will discuss project structure and configuration in more detail in the [advanced project structure]({advanced_onboarding.code_structure.path}) documentation. diff --git a/docs/images/dalle.gif b/docs/images/dalle.gif deleted file mode 100644 index 74d487849e9..00000000000 Binary files a/docs/images/dalle.gif and /dev/null differ diff --git a/docs/images/dalle_colored_code_example.png b/docs/images/dalle_colored_code_example.png deleted file mode 100644 index 18c8307c545..00000000000 Binary files a/docs/images/dalle_colored_code_example.png and /dev/null differ diff --git a/docs/images/reflex.png b/docs/images/reflex.png deleted file mode 100644 index 5812af7d836..00000000000 Binary files a/docs/images/reflex.png and /dev/null differ diff --git a/docs/images/reflex.svg b/docs/images/reflex.svg deleted file mode 100644 index f837234f8b5..00000000000 --- a/docs/images/reflex.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/images/reflex_dark.svg b/docs/images/reflex_dark.svg deleted file mode 100644 index 0aeffe59105..00000000000 --- a/docs/images/reflex_dark.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/docs/images/reflex_light.svg b/docs/images/reflex_light.svg deleted file mode 100644 index 63876bdd13e..00000000000 --- a/docs/images/reflex_light.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/docs/in/README.md b/docs/in/README.md deleted file mode 100644 index d290a585d8d..00000000000 --- a/docs/in/README.md +++ /dev/null @@ -1,249 +0,0 @@ -
-Reflex लोगो -Reflex लोगो - -
- -### **✨ प्रदर्शनकारी, अनुकूलित वेब ऐप्स, शुद्ध Python में। सेकंडों में तैनात करें। ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) - -
- ---- - -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - -# Reflex - -Reflex शुद्ध पायथन में पूर्ण-स्टैक वेब ऐप्स बनाने के लिए एक लाइब्रेरी है। - -मुख्य विशेषताएँ: - -- **शुद्ध पायथन** - अपने ऐप के फ्रंटएंड और बैकएंड को पायथन में लिखें, जावास्क्रिप्ट सीखने की जरूरत नहीं है। -- **पूर्ण लचीलापन** - Reflex के साथ शुरुआत करना आसान है, लेकिन यह जटिल ऐप्स के लिए भी स्केल कर सकता है। -- **तुरंत तैनाती** - बिल्डिंग के बाद, अपने ऐप को [एकल कमांड](https://reflex.dev/docs/hosting/deploy-quick-start/) के साथ तैनात करें या इसे अपने सर्वर पर होस्ट करें। - -Reflex के अंदर के कामकाज को जानने के लिए हमारे [आर्किटेक्चर पेज](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) को देखें। - -## ⚙️ इंस्टॉलेशन (Installation) - -एक टर्मिनल खोलें और चलाएं (Python 3.10+ की आवश्यकता है): - -```bash -pip install reflex -``` - -## 🥳 अपना पहला ऐप बनाएं (Create your first App) - -reflex को इंस्टॉल करने से ही reflex कमांड लाइन टूल भी इंस्टॉल हो जाता है। - -सुनिश्चित करें कि इंस्टॉलेशन सफल थी, एक नया प्रोजेक्ट बनाकर इसे टेस्ट करें। ('my_app_name' की जगह अपने प्रोजेक्ट का नाम रखें): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -यह कमांड आपकी नयी डायरेक्टरी में एक टेम्पलेट ऐप को प्रारंभ करता है। - -आप इस ऐप को development मोड में चला सकते हैं: - -```bash -reflex run -``` - -आपको http://localhost:3000 पर अपने ऐप को चलते हुए देखना चाहिए। - -अब आप my_app_name/my_app_name.py में source कोड को संशोधित कर सकते हैं। Reflex में तेज रिफ्रेश की सुविधा है, इसलिए जब आप अपनी कोड को सहेजते हैं, तो आप अपने बदलावों को तुरंत देख सकते हैं। - -## 🫧 उदाहरण ऐप (Example App) - -एक उदाहरण पर चलते हैं: [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node) से एक इमेज उत्पन्न करने के लिए UI। सरलता के लिए, हम सिर्फ [OpenAI API](https://platform.openai.com/docs/api-reference/authentication) को बुलाते हैं, लेकिन आप इसे ML मॉडल से बदल सकते हैं locally। - -  - -
-DALL·E के लिए एक फ्रंटएंड रैपर, छवि उत्पन्न करने की प्रक्रिया में दिखाया गया। -
- -  - -यहाँ पर इसका पूरा कोड है जिससे यह बनाया जा सकता है। यह सब एक ही Python फ़ाइल में किया गया है! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """The app state.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True - - -def index(): - return rx.center( - rx.vstack( - rx.heading("DALL-E", font_size="1.5em"), - rx.input( - placeholder="Enter a prompt..", - on_blur=State.set_prompt, - width="25em", - ), - rx.button( - "Generate Image", - on_click=State.get_image, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Add state and page to the app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## इसे समझते हैं। - -
-DALL-E ऐप के बैकएंड और फ्रंटएंड भागों के बीच के अंतर की व्याख्या करता है। -
- -### **Reflex UI** - -हम UI के साथ शुरू करेंगे। - -```python -def index(): - return rx.center( - ... - ) -``` - -यह `index` फ़ंक्शन एप्लिकेशन की फ़्रंटएंड को परिभाषित करता है। - -हम फ़्रंटएंड बनाने के लिए `center`, `vstack`, `input`, और `button` जैसे विभिन्न components का उपयोग करते हैं। Components को एक-दूसरे के भीतर डाल सकते हैं विस्तारित लेआउट बनाने के लिए। और आप CSS की पूरी ताक़त के साथ इन्हें स्टाइल करने के लिए कीवर्ड आर्ग्यूमेंट (keyword args) का उपयोग कर सकते हैं। - -रिफ़्लेक्स के पास [60+ built-in components](https://reflex.dev/docs/library) हैं जो आपको शुरुआती मदद के लिए हैं। हम बहुत से components जोड़ रहे हैं, और अपने खुद के components बनाना भी आसान है। [create your own components](https://reflex.dev/docs/wrapping-react/overview/) - -### **स्टेट (State)** - -Reflex आपके UI को आपकी स्टेट (state) के एक फ़ंक्शन के रूप में प्रस्तुत करता है। - -```python -class State(rx.State): - """The app state.""" - prompt = "" - image_url = "" - processing = False - complete = False -``` - -स्टेट (state) ऐप में उन सभी वेरिएबल्स (vars) को परिभाषित करती है जो बदल सकती हैं और उन फ़ंक्शनों को जो उन्हें बदलते हैं। - -यहां स्टेट (state) में `prompt` और `image_url` शामिल हैं। प्रगति और छवि दिखाने के लिए `processing` और `complete` बूलियन भी हैं। - -### **इवेंट हैंडलर (Event Handlers)** - -```python -def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True -``` - -स्टेट (state) के अंदर, हम इवेंट हैंडलर्स (event handlers) को परिभाषित करते हैं जो स्टेट वेरिएबल्स को बदलते हैं। इवेंट हैंडलर्स (event handlers) से reflex में स्टेट (state) को मॉडिफ़ाय किया जा सकता हैं। इन्हें उपयोगकर्ता क्रियाओं (user actions) के प्रति प्रतिक्रिया (response) के रूप में बुलाया जा सकता है, जैसे कि बटन को क्लिक करना या टेक्स्ट बॉक्स में टाइप करना। इन क्रियाओं को इवेंट्स (events) कहा जाता है। - -हमारे DALL·E. ऐप में एक इवेंट हैंडलर `get_image` है जिससे यह OpenAI API से इमेज प्राप्त करता है। इवेंट हैंडलर में `yield` का उपयोग करने कि वजह से UI अपडेट हो जाएगा। अन्यथा UI इवेंट हैंडलर के अंत में अपडेट होगा। - -### **रूटिंग (Routing)** - -आखिरकार, हम अपने एप्लिकेशन को परिभाषित करते हैं। - -```python -app = rx.App() -``` - -हम अपने एप्लिकेशन के रूट से इंडेक्स कॉम्पोनेंट तक एक पेज को जोड़ते हैं। हम एक शीर्षक भी जोड़ते हैं जो पेज प्रीव्यू/ब्राउज़र टैब में दिखाई देगा। - -```python -app.add_page(index, title="DALL-E") -``` - -आप और पेज जोड़कर एक मल्टी-पेज एप्लिकेशन बना सकते हैं। - -## 📑 संसाधन (Resources) - -
- -📑 [दस्तावेज़](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [ब्लॉग](https://reflex.dev/blog)   |   📱 [कॉम्पोनेंट लाइब्रेरी](https://reflex.dev/docs/library)   |   🖼️ [टेम्पलेट्स](https://reflex.dev/templates/)   |   🛸 [तैनाती](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ स्टेटस (Status) - -Reflex दिसंबर 2022 में Pynecone नाम से शुरू हुआ। - -2025 की शुरुआत से, [Reflex Cloud](https://cloud.reflex.dev) लॉन्च किया गया है जो Reflex ऐप्स के लिए सर्वोत्तम होस्टिंग अनुभव प्रदान करता है। हम इसे विकसित करना और अधिक सुविधाएँ लागू करना जारी रखेंगे। - -Reflex में हर सप्ताह नए रिलीज़ और फीचर्स आ रहे हैं! सुनिश्चित करें कि :star: स्टार और :eyes: वॉच इस रेपोजिटरी को अपडेट रहने के लिए। - -## (योगदान) Contributing - -हम हर तरह के योगदान का स्वागत करते हैं! रिफ्लेक्स कम्यूनिटी में शुरुआत करने के कुछ अच्छे तरीके नीचे दिए गए हैं। - -- **Join Our Discord** (डिस्कॉर्ड सर्वर से जुड़ें): हमारा [Discord](https://discord.gg/T5WSbC2YtQ) रिफ्लेक्स प्रोजेक्ट पर सहायता प्राप्त करने और आप कैसे योगदान दे सकते हैं, इस पर चर्चा करने के लिए सबसे अच्छी जगह है। -- **GitHub Discussions** (गिटहब चर्चाएँ): उन सुविधाओं के बारे में बात करने का एक शानदार तरीका जिन्हें आप जोड़ना चाहते हैं या ऐसी चीज़ें जो भ्रमित करने वाली हैं/स्पष्टीकरण की आवश्यकता है। -- **GitHub Issues** (गिटहब समस्याएं): [Issues](https://github.com/reflex-dev/reflex/issues) बग की रिपोर्ट करने का एक शानदार तरीका है। इसके अतिरिक्त, आप किसी मौजूदा समस्या को हल करने का प्रयास कर सकते हैं और एक पीआर सबमिट कर सकते हैं। - -हम सक्रिय रूप से योगदानकर्ताओं की तलाश कर रहे हैं, चाहे आपका कौशल स्तर या अनुभव कुछ भी हो। योगदान करने के लिए [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) देखें। - -## हमारे सभी योगदानकर्ताओं का धन्यवाद: - - - - - -## लाइसेंस (License) - -रिफ्लेक्स ओपन-सोर्स है और [अपाचे लाइसेंस 2.0](/LICENSE) के तहत लाइसेंस प्राप्त है। diff --git a/docs/it/README.md b/docs/it/README.md deleted file mode 100644 index 441c079b0ab..00000000000 --- a/docs/it/README.md +++ /dev/null @@ -1,250 +0,0 @@ -
-Reflex Logo -
- -### **✨ App web performanti e personalizzabili in puro Python. Distribuisci in pochi secondi. ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) - -
- ---- - -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex è una libreria per sviluppare applicazioni web full-stack in Python puro. - -Caratteristiche principali: - -- **Python Puro** - Scrivi il frontend e il backend della tua app interamente in Python, senza bisogno di imparare Javascript. -- **Flessibilità Totale** - Reflex è facile da iniziare, ma può anche gestire app complesse. -- **Distribuzione Istantanea** - Dopo lo sviluppo, distribuisci la tua app con un [singolo comando](https://reflex.dev/docs/hosting/deploy-quick-start/) o ospitala sul tuo server. - -Consulta la nostra [pagina di architettura](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) per scoprire come funziona Reflex sotto il cofano. - -## ⚙️ Installazione - -Apri un terminale ed esegui (Richiede Python 3.10+): - -```bash -pip install reflex -``` - -## 🥳 Crea la tua prima app - -Installando `reflex` si installa anche lo strumento da riga di comando `reflex`. - -Verifica che l'installazione sia stata eseguita correttamente creando un nuovo progetto. (Sostituisci `nome_app` con il nome del tuo progetto): - -```bash -mkdir nome_app -cd nome_app -reflex init -``` - -Questo comando inizializza un'app template nella tua nuova directory. - -Puoi eseguire questa app in modalità sviluppo con: - -```bash -reflex run -``` - -Dovresti vedere la tua app in esecuzione su http://localhost:3000. - -Ora puoi modificare il codice sorgente in `nome_app/nome_app.py`. Reflex offre aggiornamenti rapidi, così puoi vedere le tue modifiche istantaneamente quando salvi il tuo codice. - -## 🫧 Esempio App - -Esaminiamo un esempio: creare un'interfaccia utente per la generazione di immagini attorno a [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). Per semplicità, chiamiamo semplicemente l'[API OpenAI](https://platform.openai.com/docs/api-reference/authentication), ma potresti sostituirla con un modello ML eseguito localmente. - -  - -
-Un wrapper frontend per DALL·E, mostrato nel processo di generazione di un'immagine. -
- -  - -Ecco il codice completo per crearlo. Tutto fatto in un unico file Python! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """Lo stato dell'app.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Ottieni l'immagine dal prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Vuoto") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True - - -def index(): - return rx.center( - rx.vstack( - rx.heading("DALL-E", font_size="1.5em"), - rx.input( - placeholder="Inserisci un prompt..", - on_blur=State.set_prompt, - width="25em", - ), - rx.button( - "Genera Immagine", - on_click=State.get_image, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Aggiungi stato e pagina all'app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## Analizziamolo. - -
-Spiegazione delle differenze tra le parti backend e frontend dell'app DALL-E. -
- -### **Reflex UI** - -Cominciamo con l'UI. - -```python -def index(): - return rx.center( - ... - ) -``` - -Questa funzione `index` definisce il frontend dell'app. - -Utilizziamo diversi componenti come `center`, `vstack`, `input`, e `button` per costruire il frontend. I componenti possono essere annidati gli uni negli altri per creare layout complessi. E puoi utilizzare argomenti chiave per stilizzarli con tutta la potenza di CSS. - -Reflex offre [più di 60 componenti integrati](https://reflex.dev/docs/library) per aiutarti a iniziare. Stiamo attivamente aggiungendo più componenti ed è facile [creare i tuoi componenti](https://reflex.dev/docs/wrapping-react/overview/). - -### **Stato (State)** - -Reflex rappresenta la tua UI come una funzione del tuo stato. - -```python -class State(rx.State): - """Lo stato dell'app.""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -Lo stato definisce tutte le variabili (chiamate vars) in un'app che possono cambiare e le funzioni che le cambiano. - -Qui lo stato è composto da un `prompt` e `image_url`. Ci sono anche i booleani `processing` e `complete` per indicare quando disabilitare il pulsante (durante la generazione dell'immagine) e quando mostrare l'immagine risultante. - -### **Gestori di Eventi (Event Handlers)** - -```python -def get_image(self): - """Ottieni l'immagine dal prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Vuoto") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True -``` - -Dentro lo stato, definiamo funzioni chiamate gestori di eventi che cambiano le vars dello stato. I gestori di eventi sono il modo in cui possiamo modificare lo stato in Reflex. Possono essere chiamati in risposta alle azioni dell'utente, come fare clic su un pulsante o digitare in una casella di testo. Queste azioni vengono chiamate eventi. - -La nostra app DALL·E ha un gestore di eventi, `get_image` con cui ottiene questa immagine dall'API OpenAI. Utilizzando `yield` nel mezzo di un gestore di eventi farà sì che l'UI venga aggiornata. Altrimenti, l'UI verrà aggiornata alla fine del gestore di eventi. - -### **Instradamento (Routing)** - -Infine, definiamo la nostra app. - -```python -app = rx.App() -``` - -Aggiungiamo una pagina dalla radice dell'app al componente dell'indice. Aggiungiamo anche un titolo che apparirà nell'anteprima della pagina/scheda del browser. - -```python -app.add_page(index, title="DALL-E") -``` - -Puoi creare un'app multi-pagina aggiungendo altre pagine. - -## 📑 Risorse - -
- -📑 [Documentazione](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Libreria Componenti](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Distribuzione](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Stato - -Reflex è stato lanciato nel dicembre 2022 con il nome Pynecone. - -A partire dal 2025, [Reflex Cloud](https://cloud.reflex.dev) è stato lanciato per fornire la migliore esperienza di hosting per le app Reflex. Continueremo a svilupparlo e implementare più funzionalità. - -Reflex ha nuove versioni e funzionalità in arrivo ogni settimana! Assicurati di :star: mettere una stella e :eyes: osservare questa repository per rimanere aggiornato. - -## Contribuire - -Diamo il benvenuto a contributi di qualsiasi dimensione! Di seguito sono alcuni modi per iniziare nella comunità Reflex. - -- **Unisciti al nostro Discord**: Il nostro [Discord](https://discord.gg/T5WSbC2YtQ) è posto migliore per ottenere aiuto sul tuo progetto Reflex e per discutere come puoi contribuire. -- **Discussioni su GitHub**: Un ottimo modo per parlare delle funzionalità che desideri aggiungere o di cose che creano confusione o necessitano chiarimenti. -- **GitHub Issues**: Le [Issues](https://github.com/reflex-dev/reflex/issues) sono un ottimo modo per segnalare bug. Inoltre, puoi provare a risolvere un problema esistente e inviare un PR. - -Stiamo attivamente cercando collaboratori, indipendentemente dal tuo livello di abilità o esperienza. Per contribuire, consulta [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) - -## Un Grazie a Tutti i Nostri Contributori: - - - - - -## Licenza - -Reflex è open-source e rilasciato sotto la [Licenza Apache 2.0](/LICENSE). diff --git a/docs/ja/README.md b/docs/ja/README.md deleted file mode 100644 index 0c6be480512..00000000000 --- a/docs/ja/README.md +++ /dev/null @@ -1,250 +0,0 @@ -
-Reflex Logo -
- -### **✨ 即時デプロイが可能な、Pure Python で作ったパフォーマンスと汎用性が高い Web アプリケーション ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) - -
- ---- - -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex は Python のみでフルスタック Web アプリケーションを作成できるライブラリです。 - -主な特徴: - -- **Pure Python** - Web アプリケーションのフロントエンドとバックエンドを Python のみで実装できるため、Javascript を学ぶ必要がありません。 -- **高い柔軟性** - Reflex は簡単に始められて、複雑なアプリケーションまで作成できます。 -- **即時デプロイ** - ビルド後、すぐにデプロイが可能です。[単純な CLI コマンド](https://reflex.dev/docs/hosting/deploy-quick-start/)を使ったアプリケーションのデプロイや、自身のサーバーへのホストができます。 - -Reflex がどのように動作しているかを知るには、[アーキテクチャページ](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture)をご覧ください。 - -## ⚙️ インストール - -ターミナルを開いて以下のコマンドを実行してください。(Python 3.10 以上が必要です。): - -```bash -pip install reflex -``` - -## 🥳 最初のアプリケーションを作ろう - -`reflex`をインストールすると、`reflex`の CLI ツールが自動でインストールされます。 - -新しいプロジェクトを作成して、インストールが成功しているかを確認しましょう。(`my_app_name`を自身のプロジェクト名に書き換えて実行ください。): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -上記のコマンドを実行すると、新しいフォルダにテンプレートアプリを作成します。 - -下記のコマンドを実行すると、開発モードでアプリを開始します。 - -```bash -reflex run -``` - -http://localhost:3000 にアクセスしてアプリの動作を見ることができます。 - -`my_app_name/my_app_name.py`のソースコードを編集してみましょう!Reflex は fast refresh なので、ソースを保存した直後に変更が Web ページに反映されます。 - -## 🫧 実装例 - -実装例を見てみましょう: [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node)を中心とした画像生成 UI を作成しました。説明を簡単にするためにここでは[OpenAI API](https://platform.openai.com/docs/api-reference/authentication)を呼んでいますが、ローカルで動作している機械学習モデルに置き換えることも可能です。 - -  - -
-DALL·Eのフロントエンドラッパーです。画像を生成している過程を表示しています。 -
- -  - -画像生成 UI のソースコードの全貌を見てみましょう。下記のように、単一の Python ファイルで作れます! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """アプリのステート""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """プロンプトからイメージを取得する""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True - - -def index(): - return rx.center( - rx.vstack( - rx.heading("DALL-E", font_size="1.5em"), - rx.input( - placeholder="Enter a prompt..", - on_blur=State.set_prompt, - width="25em", - ), - rx.button( - "Generate Image", - on_click=State.get_image, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# ステートとページをアプリに追加 -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## それぞれの実装を見てみましょう - -
-DALL-E appのフロントエンドとバックエンドのパーツの違いを説明しています。 -
- -### **Reflex UI** - -UI から見てみましょう。 - -```python -def index(): - return rx.center( - ... - ) -``` - -`index`関数において、アプリのフロントエンドを定義しています。 - -フロントエンドを実装するにあたり、`center`、`vstack`、`input`、`button`など異なるコンポーネントを使用しています。コンポーネントはお互いにネストが可能であり、複雑なレイアウトを作成できます。また、keyword args を使うことで、CSS の機能をすべて使ったスタイルが可能です。 - -Reflex は[60 を超える内臓コンポーネント](https://reflex.dev/docs/library)があるため、すぐに始められます。私たちは、積極的にコンポーネントを追加していますが、簡単に[自身のコンポーネントを追加](https://reflex.dev/docs/wrapping-react/overview/)することも可能です。 - -### **ステート** - -Reflex はステートの関数を用いて UI を表示します。 - -```python -class State(rx.State): - """アプリのステート""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -ステートでは、アプリで変更が可能な全ての変数(vars と呼びます)と、vars の変更が可能な関数を定義します。 - -この例では、ステートを`prompt`と`image_url`で構成しています。そして、ブール型の`processing`と`complete`を用いて、ボタンを無効にするタイミング(画像生成中)や生成された画像を表示するタイミングを示しています。 - -### **イベントハンドラ** - -```python -def get_image(self): - """プロンプトからイメージを取得する""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True -``` - -ステートにおいて、ステートの vars を変更できるイベントハンドラ関数を定義しています。イベントハンドラは Reflex において、ステートの vars を変更する方法です。ボタンクリックやテキストボックスの入力など、ユーザのアクションに応じてイベントハンドラが呼ばれます。 - -DALL·E.アプリには、OpenAI API からイメージを取得する`get_image`関数があります。イベントハンドラの最後で UI の更新がかかるため、関数の途中に`yield`を入れることで先に UI を更新しています。 - -### **ルーティング** - -最後に、アプリを定義します。 - -```python -app = rx.App() -``` - -アプリにページを追加し、ドキュメントルートを index コンポーネントにルーティングしています。更に、ページのプレビューやブラウザタブに表示されるタイトルを記載しています。 - -```python -app.add_page(index, title="DALL-E") -``` - -ページを追加することで、マルチページアプリケーションを作成できます。 - -## 📑 リソース - -
- -📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Component Library](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ ステータス - -2022 年 12 月に、Reflex は Pynecone という名前でローンチしました。 - -2025 年から [Reflex Cloud](https://cloud.reflex.dev) がローンチされ、Reflex アプリケーションの最高のホスティング体験を提供しています。私たちは引き続き開発を続け、より多くの機能を実装していきます。 - -Reflex は毎週、新しいリリースや機能追加を行っています!最新情報を逃さないために、 :star: Star や :eyes: Watch をお願いします。 - -## コントリビュート - -様々なサイズのコントリビュートを歓迎しています!Reflex コミュニティに入るための方法を、いくつかリストアップします。 - -- **Discord に参加**: [Discord](https://discord.gg/T5WSbC2YtQ)は、Reflex プロジェクトの相談や、コントリビュートについての話し合いをするための、最適な場所です。 -- **GitHub Discussions**: GitHub Discussions では、追加したい機能や、複雑で解明が必要な事柄についての議論に適している場所です。 -- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues)はバグの報告に適している場所です。また、課題を解決した PR のサブミットにチャレンジしていただくことも、可能です。 - -スキルや経験に関わらず、私たちはコントリビュータを積極的に探しています。コントリビュートするために、[CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)をご覧ください。 - -## 私たちのコントリビュータに感謝!: - - - - - -## ライセンス - -Reflex はオープンソースであり、[Apache License 2.0](/LICENSE)に基づいてライセンス供与されます。 diff --git a/docs/kr/README.md b/docs/kr/README.md deleted file mode 100644 index 6234a075724..00000000000 --- a/docs/kr/README.md +++ /dev/null @@ -1,251 +0,0 @@ -
-Reflex Logo -
- -### **✨ 순수 Python으로 고성능 사용자 정의 웹앱을 만들어 보세요. 몇 초만에 배포 가능합니다. ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) - -
- ---- - -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex는 순수 Python으로 풀스택 웹 앱을 구축하기 위한 라이브러리입니다. - -주요 기능: - -- **순수 Python** - 앱의 프론트엔드와 백엔드를 모두 Python으로 작성하며, Javascript를 배울 필요가 없습니다. -- **완전한 유연성** - Reflex는 시작하기 쉽지만, 복잡한 앱으로도 확장할 수 있습니다. -- **즉시 배포** - 앱을 빌드한 후 [단일 명령어](https://reflex.dev/docs/hosting/deploy-quick-start/)로 배포하거나 자체 서버에서 호스팅할 수 있습니다. - -Reflex가 내부적으로 어떻게 작동하는지 알아보려면 [아키텍처 페이지](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture)를 참조하세요. - -## ⚙️ 설치 - -터미널을 열고 실행하세요. (Python 3.10+ 필요): - -```bash -pip install reflex -``` - -## 🥳 첫 앱 만들기 - -`reflex`를 설치하면, `reflex` 명령어 라인 도구도 설치됩니다. - -새 프로젝트를 생성하여 설치가 성공적인지 확인합니다. (`my_app_name`을 프로젝트 이름으로 변경합니다.): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -이 명령어는 새 디렉토리에 템플릿 앱을 초기화합니다. - -개발 모드에서 이 앱을 실행할 수 있습니다: - -```bash -reflex run -``` - -http://localhost:3000 에서 앱이 실행 됩니다. - -이제 `my_app_name/my_app_name.py`에서 소스코드를 수정할 수 있습니다. Reflex는 빠른 새로고침을 지원하므로 코드를 저장할 때마다 즉시 변경 사항을 볼 수 있습니다. - -## 🫧 예시 앱 - -예시를 살펴보겠습니다: [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node)를 중심으로 이미지 생성 UI를 만들어 보겠습니다. 간단하게 하기 위해 [OpenAI API](https://platform.openai.com/docs/api-reference/authentication)를 호출했지만, 이를 로컬에서 실행되는 ML 모델로 대체할 수 있습니다. - -  - -
-A frontend wrapper for DALL·E, shown in the process of generating an image. -
- -  - -이것이 완성된 코드입니다. 이 모든 것은 하나의 Python 파일에서 이루어집니다! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """The app state.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True - - -def index(): - return rx.center( - rx.vstack( - rx.heading("DALL-E", font_size="1.5em"), - rx.input( - placeholder="Enter a prompt..", - on_blur=State.set_prompt, - width="25em", - ), - rx.button( - "Generate Image", - on_click=State.get_image, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Add state and page to the app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## 하나씩 살펴보겠습니다. - -
-Explaining the differences between backend and frontend parts of the DALL-E app. -
- -### **Reflex UI** - -UI부터 시작해봅시다. - -```python -def index(): - return rx.center( - ... - ) -``` - -`index` 함수는 앱의 프론트엔드를 정의합니다. - -`center`, `vstack`, `input`, `button`과 같은 다양한 컴포넌트를 사용하여 프론트엔드를 구축합니다. -컴포넌트들은 복잡한 레이아웃을 만들기 위해 서로 중첩될 수 있습니다. -그리고 키워드 인자를 사용하여 CSS의 모든 기능을 사용하여 스타일을 지정할 수 있습니다. - -Reflex는 시작하기 위한 [60개 이상의 기본 컴포넌트](https://reflex.dev/docs/library)를 제공하고 있습니다. 더 많은 컴포넌트를 추가하고 있으며, [자신만의 컴포넌트를 생성하는 것](https://reflex.dev/docs/wrapping-react/overview/)도 쉽습니다. - -### **State** - -Reflex는 UI를 State 함수로 표현합니다. - -```python -class State(rx.State): - """The app state.""" - prompt = "" - image_url = "" - processing = False - complete = False -``` - -state는 앱에서 변경될 수 있는 모든 변수(vars로 불림)와 이러한 변수를 변경하는 함수를 정의합니다. - -여기서 state는 `prompt`와 `image_url`로 구성됩니다. 또한 `processing`과 `complete`라는 불리언 값이 있습니다. 이 값들은 이미지 생성 중 버튼을 비활성화할 때와, 결과 이미지를 표시할 때를 나타냅니다. - -### **Event Handlers** - -```python -def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True -``` - -State 내에서, state vars를 변경하는 이벤트 핸들러라고 불리는 함수를 정의합니다. 이벤트 핸들러는 Reflex에서 state를 변경하는 방법입니다. 버튼을 클릭하거나 텍스트 상자에 입력하는 것과 같이 사용자 동작에 응답하여 호출될 수 있습니다. 이러한 동작을 이벤트라고 합니다. - -DALL·E. 앱에는 OpenAI API에서 이미지를 가져오는 `get_image` 이벤트 핸들러가 있습니다. 이벤트 핸들러의 중간에 `yield`를 사용하면 UI가 업데이트됩니다. 그렇지 않으면 UI는 이벤트 핸들러의 끝에서 업데이트됩니다. - -### **Routing** - -마지막으로, 앱을 정의합니다. - -```python -app = rx.App() -``` - -앱의 루트에서 index 컴포넌트로 페이지를 추가합니다. 또한 페이지 미리보기/브라우저 탭에 표시될 제목도 추가합니다. - -```python -app.add_page(index, title="DALL-E") -``` - -여러 페이지를 추가하여 멀티 페이지 앱을 만들 수 있습니다. - -## 📑 자료 - -
- -📑 [문서](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [블로그](https://reflex.dev/blog)   |   📱 [컴포넌트 라이브러리](https://reflex.dev/docs/library)   |   🖼️ [템플릿](https://reflex.dev/templates/)   |   🛸 [배포](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ 상태 - -Reflex는 2022년 12월 Pynecone이라는 이름으로 출시되었습니다. - -2025년부터 [Reflex Cloud](https://cloud.reflex.dev)가 출시되어 Reflex 앱을 위한 최상의 호스팅 경험을 제공합니다. 우리는 계속해서 개발하고 더 많은 기능을 구현할 예정입니다. - -Reflex는 매주 새로운 릴리즈와 기능을 제공합니다! 최신 정보를 확인하려면 :star: Star와 :eyes: Watch를 눌러 이 저장소를 확인하세요. - -## 기여 - -우리는 모든 기여를 환영합니다! 아래는 Reflex 커뮤니티에 참여하는 좋은 방법입니다. - -- **Discord 참여**: 우리의 [Discord](https://discord.gg/T5WSbC2YtQ)는 Reflex 프로젝트에 대한 도움을 받고 기여하는 방법을 논의하는 최고의 장소입니다. -- **GitHub Discussions**: 추가하고 싶은 기능이나 혼란스럽거나 해결이 필요한 것들에 대해 이야기하는 좋은 방법입니다. -- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues)는 버그를 보고하는 훌륭한 방법입니다. 또한, 기존의 이슈를 해결하고 PR을 제출할 수 있습니다. - -우리는 능력이나 경험에 상관없이 적극적으로 기여자를 찾고 있습니다. 기여하려면 [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)를 확인하세요. - -## 모든 기여자들에게 감사드립니다: - - - - - -## License - -Reflex는 오픈소스이며 [Apache License 2.0](/LICENSE)로 라이선스가 부여됩니다. diff --git a/docs/library/data-display/avatar.md b/docs/library/data-display/avatar.md new file mode 100644 index 00000000000..725ac8f8328 --- /dev/null +++ b/docs/library/data-display/avatar.md @@ -0,0 +1,147 @@ +--- +components: + - rx.avatar +Avatar: | + lambda **props: rx.hstack(rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", **props), rx.avatar(fallback="RX", **props), spacing="3") +--- + +# Avatar + +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +from pcweb.templates.docpage import style_grid +``` + +The Avatar component is used to represent a user, and display their profile pictures or fallback texts such as initials. + +## Basic Example + +To create an avatar component with an image, pass the image URL as the `src` prop. + +```python demo +rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg") +``` + +To display a text such as initials, set the `fallback` prop without passing the `src` prop. + +```python demo +rx.avatar(fallback="RX") +``` + +## Styling + +```python eval +style_grid(component_used=rx.avatar, component_used_str="rx.avatar", variants=["solid", "soft"], fallback="RX") +``` + +### Size + +The `size` prop controls the size and spacing of the avatar. The acceptable size is from `"1"` to `"9"`, with `"3"` being the default. + +```python demo +rx.flex( + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX", size="1"), + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX", size="2"), + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX", size="3"), + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX"), + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX", size="4"), + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX", size="5"), + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX", size="6"), + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX", size="7"), + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX", size="8"), + spacing="1", +) +``` + +### Variant + +The `variant` prop controls the visual style of the avatar fallback text. The variant can be `"solid"` or `"soft"`. The default is `"soft"`. + +```python demo +rx.flex( + rx.avatar(fallback="RX", variant="solid"), + rx.avatar(fallback="RX", variant="soft"), + rx.avatar(fallback="RX"), + spacing="2", +) +``` + +### Color Scheme + +The `color_scheme` prop sets a specific color to the fallback text, ignoring the global theme. + +```python demo +rx.flex( + rx.avatar(fallback="RX", color_scheme="indigo"), + rx.avatar(fallback="RX", color_scheme="cyan"), + rx.avatar(fallback="RX", color_scheme="orange"), + rx.avatar(fallback="RX", color_scheme="crimson"), + spacing="2", +) +``` + +### High Contrast + +The `high_contrast` prop increases color contrast of the fallback text with the background. + +```python demo +rx.grid( + rx.avatar(fallback="RX", variant="solid"), + rx.avatar(fallback="RX", variant="solid", high_contrast=True), + rx.avatar(fallback="RX", variant="soft"), + rx.avatar(fallback="RX", variant="soft", high_contrast=True), + rows="2", + spacing="2", + flow="column", +) +``` + +### Radius + +The `radius` prop sets specific radius value, ignoring the global theme. It can take values `"none" | "small" | "medium" | "large" | "full"`. + +```python demo +rx.grid( + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX", radius="none"), + rx.avatar(fallback="RX", radius="none"), + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX", radius="small"), + rx.avatar(fallback="RX", radius="small"), + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX", radius="medium"), + rx.avatar(fallback="RX", radius="medium"), + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX", radius="large"), + rx.avatar(fallback="RX", radius="large"), + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RX", radius="full"), + rx.avatar(fallback="RX", radius="full"), + rows="2", + spacing="2", + flow="column", +) +``` + +### Fallback + +The `fallback` prop indicates the rendered text when the `src` cannot be loaded. + +```python demo +rx.flex( + rx.avatar(fallback="RX"), + rx.avatar(fallback="PC"), + spacing="2", +) +``` + +## Final Example + +As part of a user profile page, the Avatar component is used to display the user's profile picture, with the fallback text showing the user's initials. Text components displays the user's full name and username handle and a Button component shows the edit profile button. + +```python demo +rx.flex( + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", fallback="RU", size="9"), + rx.text("Reflex User", weight="bold", size="4"), + rx.text("@reflexuser", color_scheme="gray"), + rx.button("Edit Profile", color_scheme="indigo", variant="solid"), + direction="column", + spacing="1", +) +``` diff --git a/docs/library/data-display/badge.md b/docs/library/data-display/badge.md new file mode 100644 index 00000000000..24b8113ed39 --- /dev/null +++ b/docs/library/data-display/badge.md @@ -0,0 +1,129 @@ +--- +components: + - rx.badge + +Badge: | + lambda **props: rx.badge("Basic Badge", **props) +--- + +# Badge + +```python exec +import reflex as rx +from pcweb.templates.docpage import style_grid +``` + +Badges are used to highlight an item's status for quick recognition. + +## Basic Example + +To create a badge component with only text inside, pass the text as an argument. + +```python demo +rx.badge("New") +``` + +## Styling + +```python eval +style_grid(component_used=rx.badge, component_used_str="rx.badge", variants=["solid", "soft", "surface", "outline"], components_passed="England!",) +``` + +### Size + +The `size` prop controls the size and padding of a badge. It can take values of `"1" | "2"`, with default being `"1"`. + +```python demo +rx.flex( + rx.badge("New"), + rx.badge("New", size="1"), + rx.badge("New", size="2"), + align="center", + spacing="2", +) +``` + +### Variant + +The `variant` prop controls the visual style of the badge. The supported variant types are `"solid" | "soft" | "surface" | "outline"`. The variant default is `"soft"`. + +```python demo +rx.flex( + rx.badge("New", variant="solid"), + rx.badge("New", variant="soft"), + rx.badge("New"), + rx.badge("New", variant="surface"), + rx.badge("New", variant="outline"), + spacing="2", +) +``` + +### Color Scheme + +The `color_scheme` prop sets a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.badge("New", color_scheme="indigo"), + rx.badge("New", color_scheme="cyan"), + rx.badge("New", color_scheme="orange"), + rx.badge("New", color_scheme="crimson"), + spacing="2", +) +``` + +### High Contrast + +The `high_contrast` prop increases color contrast of the fallback text with the background. + +```python demo +rx.flex( + rx.flex( + rx.badge("New", variant="solid"), + rx.badge("New", variant="soft"), + rx.badge("New", variant="surface"), + rx.badge("New", variant="outline"), + spacing="2", + ), + rx.flex( + rx.badge("New", variant="solid", high_contrast=True), + rx.badge("New", variant="soft", high_contrast=True), + rx.badge("New", variant="surface", high_contrast=True), + rx.badge("New", variant="outline", high_contrast=True), + spacing="2", + ), + direction="column", + spacing="2", +) +``` + +### Radius + +The `radius` prop sets specific radius value, ignoring the global theme. It can take values `"none" | "small" | "medium" | "large" | "full"`. + +```python demo +rx.flex( + rx.badge("New", radius="none"), + rx.badge("New", radius="small"), + rx.badge("New", radius="medium"), + rx.badge("New", radius="large"), + rx.badge("New", radius="full"), + spacing="3", +) +``` + +## Final Example + +A badge may contain more complex elements within it. This example uses a `flex` component to align an icon and the text correctly, using the `gap` prop to +ensure a comfortable spacing between the two. + +```python demo +rx.badge( + rx.flex( + rx.icon(tag="arrow_up"), + rx.text("8.8%"), + spacing="1", + ), + color_scheme="grass", +) +``` diff --git a/docs/library/data-display/callout-ll.md b/docs/library/data-display/callout-ll.md new file mode 100644 index 00000000000..8cb58950e1d --- /dev/null +++ b/docs/library/data-display/callout-ll.md @@ -0,0 +1,139 @@ +--- +components: + - rx.callout.root + - rx.callout.icon + - rx.callout.text +--- + +```python exec +import reflex as rx +``` + +# Callout + +A `callout` is a short message to attract user's attention. + +```python demo +rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), +) +``` + +The `callout` component is made up of a `callout.root`, which groups `callout.icon` and `callout.text` parts. This component is based on the `div` element and supports common margin props. + +The `callout.icon` provides width and height for the `icon` associated with the `callout`. This component is based on the `div` element. See the [**icon** component for all icons that are available.](/docs/library/data-display/icon/) + +The `callout.text` renders the callout text. This component is based on the `p` element. + +## As alert + +```python demo +rx.callout.root( + rx.callout.icon(rx.icon(tag="triangle_alert")), + rx.callout.text("Access denied. Please contact the network administrator to view this page."), + color_scheme="red", + role="alert", +) +``` + +## Style + +### Size + +Use the `size` prop to control the size. + +```python demo +rx.flex( + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + size="3", + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + size="2", + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + size="1", + ), + direction="column", + spacing="3", + align="start", +) +``` + +### Variant + +Use the `variant` prop to control the visual style. It is set to `soft` by default. + +```python demo +rx.flex( + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + variant="soft", + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + variant="surface", + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + variant="outline", + ), + direction="column", + spacing="3", +) +``` + +### Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + color_scheme="blue", + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + color_scheme="green", + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + color_scheme="red", + ), + direction="column", + spacing="3", +) +``` + +### High Contrast + +Use the `high_contrast` prop to add additional contrast. + +```python demo +rx.flex( + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + ), + rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + high_contrast=True, + ), + direction="column", + spacing="3", +) +``` diff --git a/docs/library/data-display/callout.md b/docs/library/data-display/callout.md new file mode 100644 index 00000000000..ebd286004e4 --- /dev/null +++ b/docs/library/data-display/callout.md @@ -0,0 +1,96 @@ +--- +components: + - rx.callout + - rx.callout.root + - rx.callout.icon + - rx.callout.text + +Callout: | + lambda **props: rx.callout("Basic Callout", icon="search", **props) + +CalloutRoot: | + lambda **props: rx.callout.root( + rx.callout.icon(rx.icon(tag="info")), + rx.callout.text("You will need admin privileges to install and access this application."), + **props + ) +--- + +```python exec +import reflex as rx +from pcweb.pages import docs +``` + +# Callout + +A `callout` is a short message to attract user's attention. + +```python demo +rx.callout("You will need admin privileges to install and access this application.", icon="info") +``` + +The `icon` prop allows an icon to be passed to the `callout` component. See the [**icon** component for all icons that are available.](/docs/library/data-display/icon) + +## As alert + +```python demo +rx.callout("Access denied. Please contact the network administrator to view this page.", icon="triangle_alert", color_scheme="red", role="alert") +``` + +## Style + +### Size + +Use the `size` prop to control the size. + +```python demo +rx.flex( + rx.callout("You will need admin privileges to install and access this application.", icon="info", size="3",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", size="2",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", size="1",), + direction="column", + spacing="3", + align="start", +) +``` + +### Variant + +Use the `variant` prop to control the visual style. It is set to `soft` by default. + +```python demo +rx.flex( + rx.callout("You will need admin privileges to install and access this application.", icon="info", variant="soft",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", variant="surface",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", variant="outline",), + direction="column", + spacing="3", +) +``` + +### Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.callout("You will need admin privileges to install and access this application.", icon="info", color_scheme="blue",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", color_scheme="green",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", color_scheme="red",), + direction="column", + spacing="3", +) +``` + +### High Contrast + +Use the `high_contrast` prop to add additional contrast. + +```python demo +rx.flex( + rx.callout("You will need admin privileges to install and access this application.", icon="info",), + rx.callout("You will need admin privileges to install and access this application.", icon="info", high_contrast=True,), + direction="column", + spacing="3", +) +``` diff --git a/docs/library/data-display/code_block.md b/docs/library/data-display/code_block.md new file mode 100644 index 00000000000..d7e299aa2d9 --- /dev/null +++ b/docs/library/data-display/code_block.md @@ -0,0 +1,25 @@ +--- +components: + - rx.code_block +--- + +```python exec +import reflex as rx +``` + +# Code Block + +The Code Block component can be used to display code easily within a website. +Put in a multiline string with the correct spacing and specify and language to show the desired code. + +```python demo +rx.code_block( + """def fib(n): + if n <= 1: + return n + else: + return(fib(n-1) + fib(n-2))""", + language="python", + show_line_numbers=True, +) +``` diff --git a/docs/library/data-display/data_list.md b/docs/library/data-display/data_list.md new file mode 100644 index 00000000000..cd866a00f2c --- /dev/null +++ b/docs/library/data-display/data_list.md @@ -0,0 +1,91 @@ +--- +components: + - rx.data_list.root + - rx.data_list.item + - rx.data_list.label + - rx.data_list.value +DataListRoot: | + lambda **props: rx.data_list.root( + rx.foreach( + [["Status", "Authorized"], ["ID", "U-474747"], ["Name", "Developer Success"], ["Email", "foo@reflex.dev"]], + lambda item: rx.data_list.item(rx.data_list.label(item[0]), rx.data_list.value(item[1])), + ), + **props, + ) +DataListItem: | + lambda **props: rx.data_list.root( + rx.foreach( + [["Status", "Authorized"], ["ID", "U-474747"], ["Name", "Developer Success"], ["Email", "foo@reflex.dev"]], + lambda item: rx.data_list.item(rx.data_list.label(item[0]), rx.data_list.value(item[1]), **props), + ), + ) +DataListLabel: | + lambda **props: rx.data_list.root( + rx.foreach( + [["Status", "Authorized"], ["ID", "U-474747"], ["Name", "Developer Success"], ["Email", "foo@reflex.dev"]], + lambda item: rx.data_list.item(rx.data_list.label(item[0], **props), rx.data_list.value(item[1])), + ), + ) +DataListValue: | + lambda **props: rx.data_list.root( + rx.foreach( + [["Status", "Authorized"], ["ID", "U-474747"], ["Name", "Developer Success"], ["Email", "foo@reflex.dev"]], + lambda item: rx.data_list.item(rx.data_list.label(item[0]), rx.data_list.value(item[1], **props)), + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Data List + +The `DataList` component displays key-value pairs and is particularly helpful for showing metadata. + +A `DataList` needs to be initialized using `rx.data_list.root()` and currently takes in data list items: `rx.data_list.item` + +```python demo +rx.card( + rx.data_list.root( + rx.data_list.item( + rx.data_list.label("Status"), + rx.data_list.value( + rx.badge( + "Authorized", + variant="soft", + radius="full", + ) + ), + align="center", + ), + rx.data_list.item( + rx.data_list.label("ID"), + rx.data_list.value(rx.code("U-474747")), + ), + rx.data_list.item( + rx.data_list.label("Name"), + rx.data_list.value("Developer Success"), + align="center", + ), + rx.data_list.item( + rx.data_list.label("Email"), + rx.data_list.value( + rx.link( + "success@reflex.dev", + href="mailto:success@reflex.dev", + ), + ), + ), + rx.data_list.item( + rx.data_list.label("Company"), + rx.data_list.value( + rx.link( + "Reflex", + href="https://reflex.dev", + ), + ), + ), + ), + ), +``` diff --git a/docs/library/data-display/icon.md b/docs/library/data-display/icon.md new file mode 100644 index 00000000000..d2190117c6d --- /dev/null +++ b/docs/library/data-display/icon.md @@ -0,0 +1,194 @@ +--- +components: + - rx.lucide.Icon +--- + +```python exec +import reflex as rx +from pcweb.components.icons.lucide.lucide import lucide_icons +``` + +# Icon + +The Icon component is used to display an icon from a library of icons. This implementation is based on the [Lucide Icons](https://lucide.dev/icons) where you can find a list of all available icons. + +## Icons List + +```python eval +lucide_icons() +``` + +## Basic Example + +To display an icon, specify the `tag` prop from the list of available icons. +Passing the tag as the first children is also supported and will be assigned to the `tag` prop. + +The `tag` is expected to be in `snake_case` format, but `kebab-case` is also supported to allow copy-paste from [https://lucide.dev/icons](https://lucide.dev/icons). + +```python demo +rx.flex( + rx.icon("calendar"), + rx.icon(tag="calendar"), + gap="2", +) +``` + +## Dynamic Icons + +There are two ways to use dynamic icons in Reflex: + +### Using rx.match + +If you have a specific subset of icons you want to use dynamically, you can define an `rx.match` with them: + +```python +def dynamic_icon_with_match(icon_name): + return rx.match( + icon_name, + ("plus", rx.icon("plus")), + ("minus", rx.icon("minus")), + ("equal", rx.icon("equal")), + ) +``` + +```python exec +def dynamic_icon_with_match(icon_name): + return rx.match( + icon_name, + ("plus", rx.icon("plus")), + ("minus", rx.icon("minus")), + ("equal", rx.icon("equal")), + ) +``` + +### Using Dynamic Icon Tags + +Reflex also supports using dynamic values directly as the `tag` prop in `rx.icon()`. This allows you to use any icon from the Lucide library dynamically at runtime. + +```python exec +class DynamicIconState(rx.State): + current_icon: str = "heart" + + def change_icon(self): + icons = ["heart", "star", "bell", "calendar", "settings"] + import random + self.current_icon = random.choice(icons) +``` + +```python demo +rx.vstack( + rx.heading("Dynamic Icon Example"), + rx.icon(DynamicIconState.current_icon, size=30, color="red"), + rx.button("Change Icon", on_click=DynamicIconState.change_icon), + spacing="4", + align="center", +) +``` + +Under the hood, when a dynamic value is passed as the `tag` prop to `rx.icon()`, Reflex automatically uses a special `DynamicIcon` component that can load icons at runtime. + +```md alert +When using dynamic icons, make sure the icon names are valid. Invalid icon names will cause runtime errors. +``` + +## Styling + +Icon from Lucide can be customized with the following props `stroke_width`, `size` and `color`. + +### Stroke Width + +```python demo +rx.flex( + rx.icon("moon", stroke_width=1), + rx.icon("moon", stroke_width=1.5), + rx.icon("moon", stroke_width=2), + rx.icon("moon", stroke_width=2.5), + gap="2" +) +``` + +### Size + +```python demo +rx.flex( + rx.icon("zoom_in", size=15), + rx.icon("zoom_in", size=20), + rx.icon("zoom_in", size=25), + rx.icon("zoom_in", size=30), + align="center", + gap="2", +) +``` + +### Color + +Here is an example using basic colors in icons. + +```python demo +rx.flex( + rx.icon("zoom_in", size=18, color="indigo"), + rx.icon("zoom_in", size=18, color="cyan"), + rx.icon("zoom_in", size=18, color="orange"), + rx.icon("zoom_in", size=18, color="crimson"), + gap="2", +) +``` + +A radix color with a scale may also be specified using `rx.color()` as seen below. + +```python demo +rx.flex( + rx.icon("zoom_in", size=18, color=rx.color("purple", 1)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 2)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 3)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 4)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 5)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 6)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 7)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 8)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 9)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 10)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 11)), + rx.icon("zoom_in", size=18, color=rx.color("purple", 12)), + gap="2", +) +``` + +Here is another example using the `accent` color with scales. The `accent` is the most dominant color in your theme. + +```python demo +rx.flex( + rx.icon("zoom_in", size=18, color=rx.color("accent", 1)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 2)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 3)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 4)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 5)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 6)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 7)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 8)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 9)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 10)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 11)), + rx.icon("zoom_in", size=18, color=rx.color("accent", 12)), + gap="2", +) +``` + +## Final Example + +Icons can be used as child components of many other components. For example, adding a magnifying glass icon to a search bar. + +```python demo +rx.badge( + rx.flex( + rx.icon("search", size=18), + rx.text("Search documentation...", size="3", weight="medium"), + direction="row", + gap="1", + align="center", + ), + size="2", + radius="full", + color_scheme="gray", +) +``` diff --git a/docs/library/data-display/list.md b/docs/library/data-display/list.md new file mode 100644 index 00000000000..b5dfcf8de72 --- /dev/null +++ b/docs/library/data-display/list.md @@ -0,0 +1,70 @@ +--- +components: + - rx.list.item + - rx.list.ordered + - rx.list.unordered +--- + +```python exec +import reflex as rx +``` + +# List + +A `list` is a component that is used to display a list of items, stacked vertically by default. A `list` can be either `ordered` or `unordered`. It is based on the `flex` component and therefore inherits all of its props. + +`list.unordered` has bullet points to display the list items. + +```python demo +rx.list.unordered( + rx.list.item("Example 1"), + rx.list.item("Example 2"), + rx.list.item("Example 3"), +) +``` + +`list.ordered` has numbers to display the list items. + +```python demo +rx.list.ordered( + rx.list.item("Example 1"), + rx.list.item("Example 2"), + rx.list.item("Example 3"), +) +``` + +`list.unordered()` and `list.ordered()` can have no bullet points or numbers by setting the `list_style_type` prop to `none`. +This is effectively the same as using the `list()` component. + +```python demo +rx.hstack( + rx.list( + rx.list.item("Example 1"), + rx.list.item("Example 2"), + rx.list.item("Example 3"), + ), + rx.list.unordered( + rx.list.item("Example 1"), + rx.list.item("Example 2"), + rx.list.item("Example 3"), + list_style_type="none", + ) +) +``` + +Lists can also be used with icons. + +```python demo +rx.list( + rx.list.item( + rx.icon("circle_check_big", color="green"), " Allowed", + ), + rx.list.item( + rx.icon("octagon_x", color="red"), " Not", + ), + rx.list.item( + rx.icon("settings", color="grey"), " Settings" + ), + list_style_type="none", +) +``` diff --git a/docs/library/data-display/moment.md b/docs/library/data-display/moment.md new file mode 100644 index 00000000000..a6eeb294620 --- /dev/null +++ b/docs/library/data-display/moment.md @@ -0,0 +1,169 @@ +--- +components: + - rx.moment +--- + +# Moment + +Displaying date and relative time to now sometimes can be more complicated than necessary. + +To make it easy, Reflex is wrapping [react-moment](https://www.npmjs.com/package/react-moment) under `rx.moment`. + +```python exec +import reflex as rx +from reflex.utils.serializers import serialize_datetime +from pcweb.templates.docpage import docdemo, docdemobox, doccode, docgraphing +``` + +## Examples + +Using a date from a state var as a value, we will display it in a few different +way using `rx.moment`. + +The `date_now` state var is initialized when the site was deployed. The +button below can be used to update the var to the current datetime, which will +be reflected in the subsequent examples. + +```python demo exec +from datetime import datetime, timezone + +class MomentState(rx.State): + date_now: datetime = datetime.now(timezone.utc) + + @rx.event + def update(self): + self.date_now = datetime.now(timezone.utc) + + +def moment_update_example(): + return rx.button("Update", rx.moment(MomentState.date_now), on_click=MomentState.update) +``` + +### Display the date as-is: + +```python demo +rx.moment(MomentState.date_now) +``` + +### Humanized interval + +Sometimes we don't want to display just a raw date, but we want something more instinctive to read. That's when we can use `from_now` and `to_now`. + +```python demo +rx.moment(MomentState.date_now, from_now=True) +``` + +```python demo +rx.moment(MomentState.date_now, to_now=True) +``` + +You can also set a duration (in milliseconds) with `from_now_during` where the date will display as relative, then after that, it will be displayed as defined in `format`. + +```python demo +rx.moment(MomentState.date_now, from_now_during=100000) # after 100 seconds, date will display normally +``` + +### Formatting dates + +```python demo +rx.moment(MomentState.date_now, format="YYYY-MM-DD") +``` + +```python demo +rx.moment(MomentState.date_now, format="HH:mm:ss") +``` + +### Offset Date + +With the props `add` and `subtract`, you can pass an `rx.MomentDelta` object to modify the displayed date without affecting the stored date in your state. + +```python exec +add_example = """rx.vstack( + rx.moment(MomentState.date_now, add=rx.MomentDelta(years=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(quarters=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(months=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(months=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(months=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(weeks=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(days=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(hours=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(minutes=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, add=rx.MomentDelta(seconds=2), format="YYYY-MM-DD - HH:mm:ss"), +) +""" +subtract_example = """rx.vstack( + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(years=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(quarters=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(months=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(months=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(months=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(weeks=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(days=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(hours=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(minutes=2), format="YYYY-MM-DD - HH:mm:ss"), + rx.moment(MomentState.date_now, subtract=rx.MomentDelta(seconds=2), format="YYYY-MM-DD - HH:mm:ss"), +) +""" +``` + +```python eval +rx.tabs( + rx.tabs.list( + rx.tabs.trigger("Add", value="add"), + rx.tabs.trigger("Subtract", value="subtract") + ), + rx.tabs.content(docdemo(add_example, comp=eval(add_example)), value="add"), + rx.tabs.content(docdemo(subtract_example, comp=eval(subtract_example)), value="subtract"), + default_value="add", +) +``` + +### Timezones + +You can also set dates to display in a specific timezone: + +```python demo +rx.vstack( + rx.moment(MomentState.date_now, tz="America/Los_Angeles"), + rx.moment(MomentState.date_now, tz="Europe/Paris"), + rx.moment(MomentState.date_now, tz="Asia/Tokyo"), +) +``` + +### Client-side periodic update + +If a date is not passed to `rx.moment`, it will use the client's current time. + +If you want to update the date every second, you can use the `interval` prop. + +```python demo +rx.moment(interval=1000, format="HH:mm:ss") +``` + +Even better, you can actually link an event handler to the `on_change` prop that will be called every time the date is updated: + +```python demo exec +class MomentLiveState(rx.State): + updating: bool = False + + @rx.event + def on_update(self, date): + return rx.toast(f"Date updated: {date}") + + @rx.event + def set_updating(self, value: bool): + self.updating = value + +def moment_live_example(): + return rx.hstack( + rx.moment( + format="HH:mm:ss", + interval=rx.cond(MomentLiveState.updating, 5000, 0), + on_change=MomentLiveState.on_update, + ), + rx.switch( + is_checked=MomentLiveState.updating, + on_change=MomentLiveState.set_updating, + ), + ) +``` diff --git a/docs/library/data-display/progress.md b/docs/library/data-display/progress.md new file mode 100644 index 00000000000..aa3a1cdd691 --- /dev/null +++ b/docs/library/data-display/progress.md @@ -0,0 +1,55 @@ +--- +components: + - rx.progress + +Progress: | + lambda **props: rx.progress(value=50, **props) +--- + +# Progress + +Progress is used to display the progress status for a task that takes a long time or consists of several steps. + +```python exec +import reflex as rx +``` + +## Basic Example + +`rx.progress` expects the `value` prop to set the progress value. +`width` is default to 100%, the width of its parent component. + +```python demo +rx.vstack( + rx.progress(value=0), + rx.progress(value=50), + rx.progress(value=100), + width="50%", +) +``` + +For a dynamic progress, you can assign a state variable to the `value` prop instead of a constant value. + +```python demo exec +import asyncio + +class ProgressState(rx.State): + value: int = 0 + + @rx.event(background=True) + async def start_progress(self): + async with self: + self.value = 0 + while self.value < 100: + await asyncio.sleep(0.1) + async with self: + self.value += 1 + + +def live_progress(): + return rx.hstack( + rx.progress(value=ProgressState.value), + rx.button("Start", on_click=ProgressState.start_progress), + width="50%" + ) +``` diff --git a/docs/library/data-display/scroll_area.md b/docs/library/data-display/scroll_area.md new file mode 100644 index 00000000000..5c42be41b9b --- /dev/null +++ b/docs/library/data-display/scroll_area.md @@ -0,0 +1,237 @@ +--- +components: + - rx.scroll_area + +ScrollArea: | + lambda **props: rx.scroll_area( + rx.flex( + rx.text( + """Three fundamental aspects of typography are legibility, readability, and aesthetics. Although in a non-technical sense "legible" and "readable"are often used synonymously, typographically they are separate but related concepts.""", + size="5", + ), + rx.text( + """Legibility describes how easily individual characters can be distinguished from one another. It is described by Walter Tracy as "the quality of being decipherable and recognisable". For instance, if a "b" and an "h", or a "3" and an "8", are difficult to distinguish at small sizes, this is a problem of legibility.""", + size="5", + ), + direction="column", + spacing="4", + height="100px", + width="50%", + ), + **props + ) +--- + +```python exec +import random +import reflex as rx +from pcweb.templates.docpage import style_grid +``` + +# Scroll Area + +Custom styled, cross-browser scrollable area using native functionality. + +## Basic Example + +```python demo +rx.scroll_area( + rx.flex( + rx.text( + """Three fundamental aspects of typography are legibility, readability, and + aesthetics. Although in a non-technical sense “legible” and “readable” + are often used synonymously, typographically they are separate but + related concepts.""", + ), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as “the + quality of being decipherable and recognisable”. For instance, if a “b” + and an “h”, or a “3” and an “8”, are difficult to distinguish at small + sizes, this is a problem of legibility.""", + ), + rx.text( + """Typographers are concerned with legibility insofar as it is their job to + select the correct font to use. Brush Script is an example of a font + containing many characters that might be difficult to distinguish. The + selection of cases influences the legibility of typography because using + only uppercase letters (all-caps) reduces legibility.""", + ), + direction="column", + spacing="4", + ), + type="always", + scrollbars="vertical", + style={"height": 180}, + +) + +``` + +## Control the scrollable axes + +Use the `scrollbars` prop to limit scrollable axes. This prop can take values `"vertical" | "horizontal" | "both"`. + +```python demo +rx.grid( + rx.scroll_area( + rx.flex( + rx.text( + """Three fundamental aspects of typography are legibility, readability, and + aesthetics. Although in a non-technical sense "legible" and "readable" + are often used synonymously, typographically they are separate but + related concepts.""", + size="2", trim="both", + ), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", padding_right="48px", direction="column", spacing="4", + ), + type="always", + scrollbars="vertical", + style={"height": 150}, + ), + rx.scroll_area( + rx.flex( + rx.text( + """Three fundamental aspects of typography are legibility, readability, and + aesthetics. Although in a non-technical sense "legible" and "readable" + are often used synonymously, typographically they are separate but + related concepts.""", + size="2", trim="both", + ), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", spacing="4", style={"width": 700}, + ), + type="always", + scrollbars="horizontal", + style={"height": 150}, + ), + rx.scroll_area( + rx.flex( + rx.text( + """Three fundamental aspects of typography are legibility, readability, and + aesthetics. Although in a non-technical sense "legible" and "readable" + are often used synonymously, typographically they are separate but + related concepts.""", + size="2", trim="both", + ), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", spacing="4", style={"width": 400}, + ), + type="always", + scrollbars="both", + style={"height": 150}, + ), + columns="3", + spacing="2", +) +``` + +## Setting the type of the Scrollbars + +The `type` prop describes the nature of scrollbar visibility. + +`auto` means that scrollbars are visible when content is overflowing on the corresponding orientation. + +`always` means that scrollbars are always visible regardless of whether the content is overflowing. + +`scroll` means that scrollbars are visible when the user is scrolling along its corresponding orientation. + +`hover` when the user is scrolling along its corresponding orientation and when the user is hovering over the scroll area. + +```python demo +rx.grid( + rx.scroll_area( + rx.flex( + rx.text("type = 'auto'", weight="bold"), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", direction="column", spacing="4", + ), + type="auto", + scrollbars="vertical", + style={"height": 150}, + ), + rx.scroll_area( + rx.flex( + rx.text("type = 'always'", weight="bold"), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", direction="column", spacing="4", + ), + type="always", + scrollbars="vertical", + style={"height": 150}, + ), + rx.scroll_area( + rx.flex( + rx.text("type = 'scroll'", weight="bold"), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", direction="column", spacing="4", + ), + type="scroll", + scrollbars="vertical", + style={"height": 150}, + ), + rx.scroll_area( + rx.flex( + rx.text("type = 'hover'", weight="bold"), + rx.text( + """Legibility describes how easily individual characters can be + distinguished from one another. It is described by Walter Tracy as "the + quality of being decipherable and recognisable". For instance, if a "b" + and an "h", or a "3" and an "8", are difficult to distinguish at small + sizes, this is a problem of legibility.""", + size="2", trim="both", + ), + padding="8px", direction="column", spacing="4", + ), + type="hover", + scrollbars="vertical", + style={"height": 150}, + ), + columns="4", + spacing="2", +) + +``` diff --git a/docs/library/data-display/spinner.md b/docs/library/data-display/spinner.md new file mode 100644 index 00000000000..686a076d06b --- /dev/null +++ b/docs/library/data-display/spinner.md @@ -0,0 +1,54 @@ +--- +components: + - rx.spinner +--- + +# Spinner + +Spinner is used to display an animated loading indicator when a task is in progress. + +```python exec +import reflex as rx +``` + +```python demo +rx.spinner() +``` + +## Basic Examples + +Spinner can have different sizes. + +```python demo +rx.vstack( + rx.hstack( + rx.spinner(size="1"), + rx.spinner(size="2"), + rx.spinner(size="3"), + align="center", + gap="1em" + ) +) +``` + +## Demo with buttons + +Buttons have their own loading prop that automatically composes a spinner. + +```python demo +rx.button("Bookmark", loading=True) +``` + +## Spinner inside a button + +If you have an icon inside the button, you can use the button's disabled state and wrap the icon in a standalone rx.spinner to achieve a more sophisticated design. + +```python demo +rx.button( + rx.spinner( + loading=True + ), + "Bookmark", + disabled=True +) +``` diff --git a/docs/library/disclosure/accordion.md b/docs/library/disclosure/accordion.md new file mode 100644 index 00000000000..4a2a9eda9bb --- /dev/null +++ b/docs/library/disclosure/accordion.md @@ -0,0 +1,359 @@ +--- +components: + - rx.accordion.root + - rx.accordion.item + +AccordionRoot: | + lambda **props: rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + width="300px", + **props, + ) + +AccordionItem: | + lambda **props: rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content", **props), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", **props, + ), + rx.accordion.item(header="Third item", content="The third accordion item's content", **props), + width="300px", + ) +--- + +```python exec +import reflex as rx +``` + +# Accordion + +An accordion is a vertically stacked set of interactive headings that each reveal an associated section of content. +The accordion component is made up of `accordion`, which is the root of the component and takes in an `accordion.item`, +which contains all the contents of the collapsible section. + +## Basic Example + +```python demo +rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + width="300px", +) +``` + +## Styling + +### Type + +We use the `type` prop to determine whether multiple items can be opened at once. The allowed values for this prop are +`single` and `multiple` where `single` will only open one item at a time. The default value for this prop is `single`. + +```python demo +rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + type="multiple", +) +``` + +### Default Value + +We use the `default_value` prop to specify which item should open by default. The value for this prop should be any of the +unique values set by an `accordion.item`. + +```python demo +rx.flex( + rx.accordion.root( + rx.accordion.item( + header="First Item", + content="The first accordion item's content", + value="item_1", + ), + rx.accordion.item( + header="Second Item", + content="The second accordion item's content", + value="item_2", + ), + rx.accordion.item( + header="Third item", + content="The third accordion item's content", + value="item_3", + ), + width="300px", + default_value="item_2", + ), + direction="row", + spacing="2" +) +``` + +### Collapsible + +We use the `collapsible` prop to allow all items to close. If set to `False`, an opened item cannot be closed. + +```python demo +rx.flex( + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item(header="Second Item", content="The second accordion item's content"), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + ), + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item(header="Second Item", content="The second accordion item's content"), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=False, + width="300px", + ), + direction="row", + spacing="2" +) +``` + +### Disable + +We use the `disabled` prop to prevent interaction with the accordion and all its items. + +```python demo +rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item(header="Second Item", content="The second accordion item's content"), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + disabled=True, +) +``` + +### Orientation + +We use `orientation` prop to set the orientation of the accordion to `vertical` or `horizontal`. By default, the orientation +will be set to `vertical`. Note that, the orientation prop won't change the visual orientation but the +functional orientation of the accordion. This means that for vertical orientation, the up and down arrow keys moves focus between the next or previous item, +while for horizontal orientation, the left or right arrow keys moves focus between items. + +```python demo +rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + orientation="vertical", +) +``` + +```python demo +rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + orientation="horizontal", +) +``` + +### Variant + +```python demo +rx.flex( + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + variant="classic", + ), + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + variant="soft", + ), + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + variant="outline", + ), + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + variant="surface", + ), + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + variant="ghost", + ), + direction="row", + spacing="2" +) +``` + +### Color Scheme + +We use the `color_scheme` prop to assign a specific color to the accordion background, ignoring the global theme. + +```python demo +rx.flex( + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + color_scheme="grass", + ), + rx.accordion.root( + rx.accordion.item(header="First Item", content="The first accordion item's content"), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + color_scheme="green", + ), + direction="row", + spacing="2" +) +``` + +### Value + +We use the `value` prop to specify the controlled value of the accordion item that we want to activate. +This property should be used in conjunction with the `on_value_change` event argument. + +```python demo exec +class AccordionState(rx.State): + """The app state.""" + value: str = "item_1" + item_selected: str + + @rx.event + def change_value(self, value): + self.value = value + self.item_selected = f"{value} selected" + + +def index() -> rx.Component: + return rx.theme( + rx.container( + rx.text(AccordionState.item_selected), + rx.flex( + rx.accordion.root( + rx.accordion.item( + header="Is it accessible?", + content=rx.button("Test button"), + value="item_1", + ), + rx.accordion.item( + header="Is it unstyled?", + content="Yes. It's unstyled by default, giving you freedom over the look and feel.", + value="item_2", + ), + rx.accordion.item( + header="Is it finished?", + content="It's still in beta, but it's ready to use in production.", + value="item_3", + ), + collapsible=True, + width="300px", + value=AccordionState.value, + on_value_change=lambda value: AccordionState.change_value(value), + ), + direction="column", + spacing="2", + ), + padding="2em", + text_align="center", + ) + ) +``` + +## AccordionItem + +The accordion item contains all the parts of a collapsible section. + +## Styling + +### Value + +```python demo +rx.accordion.root( + rx.accordion.item( + header="First Item", + content="The first accordion item's content", + value="item_1", + ), + rx.accordion.item( + header="Second Item", + content="The second accordion item's content", + value="item_2", + ), + rx.accordion.item( + header="Third item", + content="The third accordion item's content", + value="item_3", + ), + collapsible=True, + width="300px", +) +``` + +### Disable + +```python demo +rx.accordion.root( + rx.accordion.item( + header="First Item", + content="The first accordion item's content", + disabled=True, + ), + rx.accordion.item( + header="Second Item", content="The second accordion item's content", + ), + rx.accordion.item(header="Third item", content="The third accordion item's content"), + collapsible=True, + width="300px", + color_scheme="blue", +) +``` diff --git a/docs/library/disclosure/segmented_control.md b/docs/library/disclosure/segmented_control.md new file mode 100644 index 00000000000..9740c556d21 --- /dev/null +++ b/docs/library/disclosure/segmented_control.md @@ -0,0 +1,56 @@ +--- +components: + - rx.segmented_control.root + - rx.segmented_control.item +--- + +```python exec +import reflex as rx + +class SegmentedState(rx.State): + """The app state.""" + + control: str = "test" + + @rx.event + def set_control(self, value: str | list[str]): + self.control = value + + +``` + +# Segmented Control + +Segmented Control offers a clear and accessible way to switch between predefined values and views, e.g., "Inbox," "Drafts," and "Sent." + +With Segmented Control, you can make mutually exclusive choices, where only one option can be active at a time, clear and accessible. Without Segmented Control, end users might have to deal with controls like dropdowns or multiple buttons that don't clearly convey state or group options together visually. + +## Basic Example + +The `Segmented Control` component is made up of a `rx.segmented_control.root` which groups `rx.segmented_control.item`. + +The `rx.segmented_control.item` components define the individual segments of the control, each with a label and a unique value. + +```python demo +rx.vstack( + rx.segmented_control.root( + rx.segmented_control.item("Home", value="home"), + rx.segmented_control.item("About", value="about"), + rx.segmented_control.item("Test", value="test"), + on_change=SegmentedState.set_control, + value=SegmentedState.control, + ), + rx.card( + rx.text(SegmentedState.control, align="left"), + rx.text(SegmentedState.control, align="center"), + rx.text(SegmentedState.control, align="right"), + width="100%", + ), +) +``` + +**In the example above:** + +`on_change` is used to specify a callback function that will be called when the user selects a different segment. In this case, the `SegmentedState.setvar("control")` function is used to update the `control` state variable when the user changes the selected segment. + +`value` prop is used to specify the currently selected segment, which is bound to the `SegmentedState.control` state variable. diff --git a/docs/library/disclosure/tabs.md b/docs/library/disclosure/tabs.md new file mode 100644 index 00000000000..05fdb8a67be --- /dev/null +++ b/docs/library/disclosure/tabs.md @@ -0,0 +1,330 @@ +--- +components: + - rx.tabs.root + - rx.tabs.list + - rx.tabs.trigger + - rx.tabs.content + +only_low_level: + - True + +TabsRoot: | + lambda **props: rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Account", value="account"), + rx.tabs.trigger("Documents", value="documents"), + rx.tabs.trigger("Settings", value="settings"), + ), + rx.box( + rx.tabs.content( + rx.text("Make changes to your account"), + value="account", + ), + rx.tabs.content( + rx.text("Update your documents"), + value="documents", + ), + rx.tabs.content( + rx.text("Edit your personal profile"), + value="settings", + ), + ), + **props, + ) + +TabsList: | + lambda **props: rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Account", value="account"), + rx.tabs.trigger("Documents", value="documents"), + rx.tabs.trigger("Settings", value="settings"), + **props, + ), + rx.box( + rx.tabs.content( + rx.text("Make changes to your account"), + value="account", + ), + rx.tabs.content( + rx.text("Update your documents"), + value="documents", + ), + rx.tabs.content( + rx.text("Edit your personal profile"), + value="settings", + ), + ), + ) + +TabsTrigger: | + lambda **props: rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Account", value="account", **props,), + rx.tabs.trigger("Documents", value="documents"), + rx.tabs.trigger("Settings", value="settings"), + ), + rx.box( + rx.tabs.content( + rx.text("Make changes to your account"), + value="account", + ), + rx.tabs.content( + rx.text("Update your documents"), + value="documents", + ), + rx.tabs.content( + rx.text("Edit your personal profile"), + value="settings", + ), + ), + ) + +TabsContent: | + lambda **props: rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Account", value="account"), + rx.tabs.trigger("Documents", value="documents"), + rx.tabs.trigger("Settings", value="settings"), + ), + rx.box( + rx.tabs.content( + rx.text("Make changes to your account"), + value="account", + **props, + ), + rx.tabs.content( + rx.text("Update your documents"), + value="documents", + **props, + ), + rx.tabs.content( + rx.text("Edit your personal profile"), + value="settings", + **props, + ), + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Tabs + +Tabs are a set of layered sections of content—known as tab panels that are displayed one at a time. +They facilitate the organization and navigation between sets of content that share a connection and exist at a similar level of hierarchy. + +## Basic Example + +```python demo +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2") + ), + rx.tabs.content( + rx.text("item on tab 1"), + value="tab1", + ), + rx.tabs.content( + rx.text("item on tab 2"), + value="tab2", + ), +) + +``` + +The `tabs` component is made up of a `rx.tabs.root` which groups `rx.tabs.list` and `rx.tabs.content` parts. + +## Styling + +### Default value + +We use the `default_value` prop to set a default active tab, this will select the specified tab by default. + +```python demo +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2") + ), + rx.tabs.content( + rx.text("item on tab 1"), + value="tab1", + ), + rx.tabs.content( + rx.text("item on tab 2"), + value="tab2", + ), + default_value="tab2", +) +``` + +### Orientation + +We use `orientation` prop to set the orientation of the tabs component to `vertical` or `horizontal`. By default, the orientation +will be set to `horizontal`. Setting this value will change both the visual orientation and the functional orientation. + +```md alert info +The functional orientation means for `vertical`, the `up` and `down` arrow keys moves focus between the next or previous tab, +while for `horizontal`, the `left` and `right` arrow keys moves focus between tabs. +``` + +```md alert warning +# When using vertical orientation, make sure to assign a tabs.content for each trigger. + +Defining triggers without content will result in a visual bug where the width of the triggers list isn't constant. +``` + +```python demo +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2") + ), + rx.tabs.content( + rx.text("item on tab 1"), + value="tab1", + ), + rx.tabs.content( + rx.text("item on tab 2"), + value="tab2", + ), + default_value="tab1", + orientation="vertical", +) +``` + +```python demo +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2") + ), + rx.tabs.content( + rx.text("item on tab 1"), + value="tab1", + ), + rx.tabs.content( + rx.text("item on tab 2"), + value="tab2", + ), + default_value="tab1", + orientation="horizontal", +) +``` + +### Value + +We use the `value` prop to specify the controlled value of the tab that we want to activate. This property should be used in conjunction with the `on_change` event argument. + +```python demo exec +class TabsState(rx.State): + """The app state.""" + + value = "tab1" + tab_selected = "" + + @rx.event + def change_value(self, val): + self.tab_selected = f"{val} clicked!" + self.value = val + + +def index() -> rx.Component: + return rx.container( + rx.flex( + rx.text(f"{TabsState.value} clicked !"), + rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2"), + ), + rx.tabs.content( + rx.text("items on tab 1"), + value="tab1", + ), + rx.tabs.content( + rx.text("items on tab 2"), + value="tab2", + ), + default_value="tab1", + value=TabsState.value, + on_change=lambda x: TabsState.change_value(x), + ), + direction="column", + spacing="2", + ), + padding="2em", + font_size="2em", + text_align="center", + ) +``` + +## Tablist + +The Tablist is used to list the respective tabs to the tab component + +## Tab Trigger + +This is the button that activates the tab's associated content. It is typically used in the `Tablist` + +## Styling + +### Value + +We use the `value` prop to assign a unique value that associates the trigger with content. + +```python demo +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2"), + rx.tabs.trigger("Tab 3", value="tab3") + ), +) +``` + +### Disable + +We use the `disabled` prop to disable the tab. + +```python demo +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2"), + rx.tabs.trigger("Tab 3", value="tab3", disabled=True) + ), +) +``` + +## Tabs Content + +Contains the content associated with each trigger. + +## Styling + +### Value + +We use the `value` prop to assign a unique value that associates the content with a trigger. + +```python +rx.tabs.root( + rx.tabs.list( + rx.tabs.trigger("Tab 1", value="tab1"), + rx.tabs.trigger("Tab 2", value="tab2") + ), + rx.tabs.content( + rx.text("item on tab 1"), + value="tab1", + ), + rx.tabs.content( + rx.text("item on tab 2"), + value="tab2", + ), + default_value="tab1", + orientation="vertical", +) +``` diff --git a/docs/library/dynamic-rendering/auto_scroll.md b/docs/library/dynamic-rendering/auto_scroll.md new file mode 100644 index 00000000000..a2294ca539c --- /dev/null +++ b/docs/library/dynamic-rendering/auto_scroll.md @@ -0,0 +1,70 @@ +```python exec +import reflex as rx +``` + +# Auto Scroll + +The `rx.auto_scroll` component is a div that automatically scrolls to the bottom when new content is added. This is useful for chat interfaces, logs, or any container where new content is dynamically added and you want to ensure the most recent content is visible. + +## Basic Usage + +```python demo exec +import reflex as rx + +class AutoScrollState(rx.State): + messages: list[str] = ["Initial message"] + + def add_message(self): + self.messages.append(f"New message #{len(self.messages) + 1}") + +def auto_scroll_example(): + return rx.vstack( + rx.auto_scroll( + rx.foreach( + AutoScrollState.messages, + lambda message: rx.box( + message, + padding="0.5em", + border_bottom="1px solid #eee", + width="100%" + ) + ), + height="200px", + width="300px", + border="1px solid #ddd", + border_radius="md", + ), + rx.button("Add Message", on_click=AutoScrollState.add_message), + width="300px", + align_items="center", + ) +``` + +The `auto_scroll` component automatically scrolls to show the newest content when it's added. In this example, each time you click "Add Message", a new message is added to the list and the container automatically scrolls to display it. + +## When to Use Auto Scroll + +- **Chat applications**: Keep the chat window scrolled to the most recent messages. +- **Log viewers**: Automatically follow new log entries as they appear. +- **Feed interfaces**: Keep the newest content visible in dynamically updating feeds. + +## Props + +`rx.auto_scroll` is based on the `rx.div` component and inherits all of its props. By default, it sets `overflow="auto"` to enable scrolling. + +Some common props you might use with `auto_scroll`: + +- `height`: Set the height of the scrollable container. +- `width`: Set the width of the scrollable container. +- `padding`: Add padding inside the container. +- `border`: Add a border around the container. +- `border_radius`: Round the corners of the container. + +## How It Works + +The component tracks when new content is added and maintains the scroll position in two scenarios: + +1. When the user is already near the bottom of the content (within 50 pixels), it will scroll to the bottom when new content is added. +2. When the container didn't have a scrollbar before but does now (due to new content), it will automatically scroll to the bottom. + +This behavior ensures that users can scroll up to view older content without being forced back to the bottom, while still automatically following new content in most cases. diff --git a/docs/library/dynamic-rendering/cond.md b/docs/library/dynamic-rendering/cond.md new file mode 100644 index 00000000000..1fbd4dab8e8 --- /dev/null +++ b/docs/library/dynamic-rendering/cond.md @@ -0,0 +1,161 @@ +```python exec +import reflex as rx +``` + +# Cond + +This component is used to conditionally render components. + +The cond component takes a condition and two components. +If the condition is `True`, the first component is rendered, otherwise the second component is rendered. + +```python demo exec +class CondState(rx.State): + show: bool = True + + @rx.event + def change(self): + self.show = not (self.show) + + +def cond_example(): + return rx.vstack( + rx.button("Toggle", on_click=CondState.change), + rx.cond(CondState.show, rx.text("Text 1", color="blue"), rx.text("Text 2", color="red")), + ) +``` + +The second component is optional and can be omitted. +If it is omitted, nothing is rendered if the condition is `False`. + +```python demo exec +class CondOptionalState(rx.State): + show_optional: bool = True + + @rx.event + def toggle_optional(self): + self.show_optional = not (self.show_optional) + + +def cond_optional_example(): + return rx.vstack( + rx.button("Toggle", on_click=CondOptionalState.toggle_optional), + rx.cond(CondOptionalState.show_optional, rx.text("This text appears when condition is True", color="green")), + rx.text("This text is always visible", color="gray"), + ) +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=6040&end=6463 +# Video: Conditional Rendering +``` + +## Negation + +You can use the logical operator `~` to negate a condition. + +```python +rx.vstack( + rx.button("Toggle", on_click=CondState.change), + rx.cond(CondState.show, rx.text("Text 1", color="blue"), rx.text("Text 2", color="red")), + rx.cond(~CondState.show, rx.text("Text 1", color="blue"), rx.text("Text 2", color="red")), +) +``` + +## Multiple Conditions + +It is also possible to make up complex conditions using the `logical or` (|) and `logical and` (&) operators. + +Here we have an example using the var operators `>=`, `<=`, `&`. We define a condition that if a person has an age between 18 and 65, including those ages, they are able to work, otherwise they cannot. + +We could equally use the operator `|` to represent a `logical or` in one of our conditions. + +```python demo exec +import random + +class CondComplexState(rx.State): + age: int = 19 + + @rx.event + def change(self): + self.age = random.randint(0, 100) + + +def cond_complex_example(): + return rx.vstack( + rx.button("Toggle", on_click=CondComplexState.change), + rx.text(f"Age: {CondComplexState.age}"), + rx.cond( + (CondComplexState.age >= 18) & (CondComplexState.age <=65), + rx.text("You can work!", color="green"), + rx.text("You cannot work!", color="red"), + ), + ) + +``` + +## Nested Conditional + +We can also nest `cond` components within each other to create more complex logic. In python we can have an `if` statement that then has several `elif` statements before finishing with an `else`. This is also possible in reflex using nested `cond` components. In this example we check whether a number is positive, negative or zero. + +Here is the python logic using `if` statements: + +```python +number = 0 + +if number > 0: + print("Positive number") + +elif number == 0: + print('Zero') +else: + print('Negative number') +``` + +This reflex code that is logically identical: + +```python demo exec +import random + + +class NestedState(rx.State): + + num: int = 0 + + def change(self): + self.num = random.randint(-10, 10) + + +def cond_nested_example(): + return rx.vstack( + rx.button("Toggle", on_click=NestedState.change), + rx.cond( + NestedState.num > 0, + rx.text(f"{NestedState.num} is Positive!", color="orange"), + rx.cond( + NestedState.num == 0, + rx.text(f"{NestedState.num} is Zero!", color="blue"), + rx.text(f"{NestedState.num} is Negative!", color="red"), + ) + ), + ) + +``` + +Here is a more advanced example where we have three numbers and we are checking which of the three is the largest. If any two of them are equal then we return that `Some of the numbers are equal!`. + +The reflex code that follows is logically identical to doing the following in python: + +```python +a = 8 +b = 10 +c = 2 + +if((a>b and a>c) and (a != b and a != c)): + print(a, " is the largest!") +elif((b>a and b>c) and (b != a and b != c)): + print(b, " is the largest!") +elif((c>a and c>b) and (c != a and c != b)): + print(c, " is the largest!") +else: + print("Some of the numbers are equal!") +``` diff --git a/docs/library/dynamic-rendering/foreach.md b/docs/library/dynamic-rendering/foreach.md new file mode 100644 index 00000000000..7b107e86c3e --- /dev/null +++ b/docs/library/dynamic-rendering/foreach.md @@ -0,0 +1,199 @@ +```python exec +import reflex as rx +``` + +# Foreach + +The `rx.foreach` component takes an iterable(list, tuple or dict) and a function that renders each item in the list. +This is useful for dynamically rendering a list of items defined in a state. + +```md alert warning +# `rx.foreach` is specialized for usecases where the iterable is defined in a state var. + +For an iterable where the content doesn't change at runtime, i.e a constant, using a list/dict comprehension instead of `rx.foreach` is preferred. +``` + +```python demo exec +from typing import List +class ForeachState(rx.State): + color: List[str] = ["red", "green", "blue", "yellow", "orange", "purple"] + +def colored_box(color: str): + return rx.box( + rx.text(color), + bg=color + ) + +def foreach_example(): + return rx.grid( + rx.foreach( + ForeachState.color, + colored_box + ), + columns="2", + ) +``` + +The function can also take an index as a second argument. + +```python demo exec +def colored_box_index(color: str, index: int): + return rx.box( + rx.text(index), + bg=color + ) + +def foreach_example_index(): + return rx.grid( + rx.foreach( + ForeachState.color, + lambda color, index: colored_box_index(color, index) + ), + columns="2", + ) +``` + +Nested foreach components can be used to render nested lists. + +When indexing into a nested list, it's important to declare the list's type as Reflex requires it for type checking. +This ensures that any potential frontend JS errors are caught before the user can encounter them. + +```python demo exec +from typing import List + +class NestedForeachState(rx.State): + numbers: List[List[str]] = [["1", "2", "3"], ["4", "5", "6"], ["7", "8", "9"]] + +def display_row(row): + return rx.hstack( + rx.foreach( + row, + lambda item: rx.box( + item, + border="1px solid black", + padding="0.5em", + ) + ), + ) + +def nested_foreach_example(): + return rx.vstack( + rx.foreach( + NestedForeachState.numbers, + display_row + ) + ) +``` + +Below is a more complex example of foreach within a todo list. + +```python demo exec +from typing import List +class ListState(rx.State): + items: List[str] = ["Write Code", "Sleep", "Have Fun"] + new_item: str + + @rx.event + def set_new_item(self, new_item: str): + self.new_item = new_item + + @rx.event + def add_item(self): + self.items += [self.new_item] + + def finish_item(self, item: str): + self.items = [i for i in self.items if i != item] + +def get_item(item): + return rx.list.item( + rx.hstack( + rx.button( + on_click=lambda: ListState.finish_item(item), + height="1.5em", + background_color="white", + border="1px solid blue", + ), + rx.text(item, font_size="1.25em"), + ), + ) + +def todo_example(): + return rx.vstack( + rx.heading("Todos"), + rx.input(on_blur=ListState.set_new_item, placeholder="Add a todo...", bg = "white"), + rx.button("Add", on_click=ListState.add_item, bg = "white"), + rx.divider(), + rx.list.ordered( + rx.foreach( + ListState.items, + get_item, + ), + ), + bg = "#ededed", + padding = "1em", + border_radius = "0.5em", + shadow = "lg" + ) +``` + +## Dictionaries + +Items in a dictionary can be accessed as list of key-value pairs. +Using the color example, we can slightly modify the code to use dicts as shown below. + +```python demo exec +from typing import List +class SimpleDictForeachState(rx.State): + color_chart: dict[int, str] = { + 1 : "blue", + 2: "red", + 3: "green" + } + +def display_color(color: List): + # color is presented as a list key-value pair([1, "blue"],[2, "red"], [3, "green"]) + return rx.box(rx.text(color[0]), bg=color[1]) + + +def foreach_dict_example(): + return rx.grid( + rx.foreach( + SimpleDictForeachState.color_chart, + display_color + ), + columns = "2" + ) +``` + +Now let's show a more complex example with dicts using the color example. +Assuming we want to display a dictionary of secondary colors as keys and their constituent primary colors as values, we can modify the code as below: + +```python demo exec +from typing import List, Dict +class ComplexDictForeachState(rx.State): + color_chart: Dict[str, List[str]] = { + "purple": ["red", "blue"], + "orange": ["yellow", "red"], + "green": ["blue", "yellow"] + } + +def display_colors(color: List): + return rx.vstack( + rx.text(color[0], color=color[0]), + rx.hstack( + rx.foreach( + color[1], lambda x: rx.box(rx.text(x, color="black"), bg=x) + ) + + ) + ) + +def foreach_complex_dict_example(): + return rx.grid( + rx.foreach( + ComplexDictForeachState.color_chart, + display_colors + ), + columns="2" + ) +``` diff --git a/docs/library/dynamic-rendering/match.md b/docs/library/dynamic-rendering/match.md new file mode 100644 index 00000000000..d258d68d64b --- /dev/null +++ b/docs/library/dynamic-rendering/match.md @@ -0,0 +1,295 @@ +```python exec +import reflex as rx +``` + +# Match + +The `rx.match` feature in Reflex serves as a powerful alternative to `rx.cond` for handling conditional statements. +While `rx.cond` excels at conditionally rendering two components based on a single condition, +`rx.match` extends this functionality by allowing developers to handle multiple conditions and their associated components. +This feature is especially valuable when dealing with intricate conditional logic or nested scenarios, +where the limitations of `rx.cond` might lead to less readable and more complex code. + +With `rx.match`, developers can not only handle multiple conditions but also perform structural pattern matching, +making it a versatile tool for managing various scenarios in Reflex applications. + +## Basic Usage + +The `rx.match` function provides a clear and expressive syntax for handling multiple +conditions and their corresponding components: + +```python +rx.match( + condition, + (case_1, component_1), + (case_2, component_2), + ... + default_component, +) + +``` + +- `condition`: The value to match against. +- `(case_i, component_i)`: A Tuple of matching cases and their corresponding return components. +- `default_component`: A special case for the default component when the condition isn't matched by any of the match cases. + +Example + +```python demo exec +from typing import List + +import reflex as rx + + +class MatchState(rx.State): + cat_breed: str = "" + animal_options: List[str] = ["persian", "siamese", "maine coon", "ragdoll", "pug", "corgi"] + + @rx.event + def set_cat_breed(self, breed: str): + self.cat_breed = breed + +def match_demo(): + return rx.flex( + rx.match( + MatchState.cat_breed, + ("persian", rx.text("Persian cat selected.")), + ("siamese", rx.text("Siamese cat selected.")), + ("maine coon", rx.text("Maine Coon cat selected.")), + ("ragdoll", rx.text("Ragdoll cat selected.")), + rx.text("Unknown cat breed selected.") + ), + rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.foreach(MatchState.animal_options, lambda x: rx.select.item(x, value=x)) + ), + ), + value=MatchState.cat_breed, + on_change=MatchState.set_cat_breed, + + ), + direction= "column", + gap= "2" + ) +``` + +## Default Case + +The default case in `rx.match` serves as a universal handler for scenarios where none of +the specified match cases aligns with the given match condition. Here are key considerations +when working with the default case: + +- **Placement in the Match Function**: The default case must be the last non-tuple argument in the `rx.match` component. + All match cases should be enclosed in tuples; any non-tuple value is automatically treated as the default case. For example: + +```python +rx.match( + MatchState.cat_breed, + ("persian", rx.text("persian cat selected")), + rx.text("Unknown cat breed selected."), + ("siamese", rx.text("siamese cat selected")), + ) +``` + +The above code snippet will result in an error due to the misplaced default case. + +- **Single Default Case**: Only one default case is allowed in the `rx.match` component. + Attempting to specify multiple default cases will lead to an error. For instance: + +```python +rx.match( + MatchState.cat_breed, + ("persian", rx.text("persian cat selected")), + ("siamese", rx.text("siamese cat selected")), + rx.text("Unknown cat breed selected."), + rx.text("Another unknown cat breed selected.") + ) +``` + +- **Optional Default Case for Component Return Values**: If the match cases in a `rx.match` component + return components, the default case can be optional. In this scenario, if a default case is + not provided, `rx.fragment` will be implicitly assigned as the default. For example: + +```python +rx.match( + MatchState.cat_breed, + ("persian", rx.text("persian cat selected")), + ("siamese", rx.text("siamese cat selected")), + ) +``` + +In this case, `rx.fragment` is the default case. However, not providing a default case for non-component +return values will result in an error: + +```python +rx.match( + MatchState.cat_breed, + ("persian", "persian cat selected"), + ("siamese", "siamese cat selected"), + ) +``` + +The above code snippet will result in an error as a default case must be explicitly +provided in this scenario. + +## Handling Multiple Conditions + +`rx.match` excels in efficiently managing multiple conditions and their corresponding components, +providing a cleaner and more readable alternative compared to nested `rx.cond` structures. + +Consider the following example: + +```python demo exec +from typing import List + +import reflex as rx + + +class MultiMatchState(rx.State): + animal_breed: str = "" + animal_options: List[str] = ["persian", "siamese", "maine coon", "pug", "corgi", "mustang", "rahvan", "football", "golf"] + + @rx.event + def set_animal_breed(self, breed: str): + self.animal_breed = breed + +def multi_match_demo(): + return rx.flex( + rx.match( + MultiMatchState.animal_breed, + ("persian", "siamese", "maine coon", rx.text("Breeds of cats.")), + ("pug", "corgi", rx.text("Breeds of dogs.")), + ("mustang", "rahvan", rx.text("Breeds of horses.")), + rx.text("Unknown animal breed") + ), + rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.foreach(MultiMatchState.animal_options, lambda x: rx.select.item(x, value=x)) + ), + ), + value=MultiMatchState.animal_breed, + on_change=MultiMatchState.set_animal_breed, + + ), + direction= "column", + gap= "2" + ) +``` + +In a match case tuple, you can specify multiple conditions. The last value of the match case +tuple is automatically considered as the return value. It's important to note that a match case +tuple should contain a minimum of two elements: a match case and a return value. +The following code snippet will result in an error: + +```python +rx.match( + MatchState.cat_breed, + ("persian",), + ("maine coon", rx.text("Maine Coon cat selected")), + ) +``` + +## Usage as Props + +Similar to `rx.cond`, `rx.match` can be used as prop values, allowing dynamic behavior for UI components: + +```python demo exec +import reflex as rx + + +class MatchPropState(rx.State): + value: int = 0 + + @rx.event + def incr(self): + self.value += 1 + + @rx.event + def decr(self): + self.value -= 1 + + +def match_prop_demo_(): + return rx.flex( + rx.button("decrement", on_click=MatchPropState.decr, background_color="red"), + rx.badge( + MatchPropState.value, + color_scheme= rx.match( + MatchPropState.value, + (1, "red"), + (2, "blue"), + (6, "purple"), + (10, "orange"), + "green" + ), + size="2", + ), + rx.button("increment", on_click=MatchPropState.incr), + align_items="center", + direction= "row", + gap= "3" + ) +``` + +In the example above, the background color property of the box component containing `State.value` changes to red when +`state.value` is 1, blue when its 5, green when its 5, orange when its 15 and black for any other value. + +The example below also shows handling multiple conditions with the match component as props. + +```python demo exec +import reflex as rx + + +class MatchMultiPropState(rx.State): + value: int = 0 + + @rx.event + def incr(self): + self.value += 1 + + @rx.event + def decr(self): + self.value -= 1 + + +def match_multi_prop_demo_(): + return rx.flex( + rx.button("decrement", on_click=MatchMultiPropState.decr, background_color="red"), + rx.badge( + MatchMultiPropState.value, + color_scheme= rx.match( + MatchMultiPropState.value, + (1, 3, 9, "red"), + (2, 4, 5, "blue"), + (6, 8, 12, "purple"), + (10, 15, 20, 25, "orange"), + "green" + ), + size="2", + ), + rx.button("increment", on_click=MatchMultiPropState.incr), + align_items="center", + direction= "row", + gap= "3" + ) +``` + +```md alert warning +# Usage with Structural Pattern Matching + +The `rx.match` component is designed for structural pattern matching. If the value of your match condition evaluates to a boolean (True or False), it is recommended to use `rx.cond` instead. + +Consider the following example, which is more suitable for `rx.cond`:\* +``` + +```python +rx.cond( + MatchPropState.value == 10, + "true value", + "false value" +) +``` diff --git a/docs/library/forms/button.md b/docs/library/forms/button.md new file mode 100644 index 00000000000..7793fe81e99 --- /dev/null +++ b/docs/library/forms/button.md @@ -0,0 +1,63 @@ +--- +components: + - rx.button + +Button: | + lambda **props: rx.button("Basic Button", **props) +--- + +```python exec +import reflex as rx +``` + +# Button + +Buttons are essential elements in your application's user interface that users can click to trigger events. + +## Basic Example + +The `on_click` trigger is called when the button is clicked. + +```python demo exec +class CountState(rx.State): + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + + @rx.event + def decrement(self): + self.count -= 1 + +def counter(): + return rx.flex( + rx.button( + "Decrement", + color_scheme="red", + on_click=CountState.decrement, + ), + rx.heading(CountState.count), + rx.button( + "Increment", + color_scheme="grass", + on_click=CountState.increment, + ), + spacing="3", + ) +``` + +### Loading and Disabled + +The `loading` prop is used to indicate that the action triggered by the button is currently in progress. When set to `True`, the button displays a loading spinner, providing visual feedback to the user that the action is being processed. This also prevents multiple clicks while the button is in the loading state. By default, `loading` is set to `False`. + +The `disabled` prop also prevents the button from being but does not provide a spinner. + +```python demo +rx.flex( + rx.button("Regular"), + rx.button("Loading", loading=True), + rx.button("Disabled", disabled=True), + spacing="2", +) +``` diff --git a/docs/library/forms/checkbox.md b/docs/library/forms/checkbox.md new file mode 100644 index 00000000000..ef00cff7026 --- /dev/null +++ b/docs/library/forms/checkbox.md @@ -0,0 +1,73 @@ +--- +components: + - rx.checkbox + +HighLevelCheckbox: | + lambda **props: rx.checkbox("Basic Checkbox", **props) +--- + +```python exec +import reflex as rx +``` + +# Checkbox + +## Basic Example + +The `on_change` trigger is called when the `checkbox` is clicked. + +```python demo exec +class CheckboxState(rx.State): + checked: bool = False + + @rx.event + def set_checked(self, value: bool): + self.checked = value + +def checkbox_example(): + return rx.vstack( + rx.heading(CheckboxState.checked), + rx.checkbox(on_change=CheckboxState.set_checked), + ) +``` + +The `input` prop is used to set the `checkbox` as a controlled component. + +```python demo exec +class FormCheckboxState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + print(form_data) + self.form_data = form_data + + +def form_checkbox_example(): + return rx.card( + rx.vstack( + rx.heading("Example Form"), + rx.form.root( + rx.hstack( + rx.checkbox( + name="checkbox", + label="Accept terms and conditions", + ), + rx.button("Submit", type="submit"), + width="100%", + ), + on_submit=FormCheckboxState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.hstack( + rx.heading("Results:"), + rx.badge(FormCheckboxState.form_data.to_string()), + ), + align_items="left", + width="100%", + ), + width="50%", + ) +``` diff --git a/docs/library/forms/form-ll.md b/docs/library/forms/form-ll.md new file mode 100644 index 00000000000..8e5dfdd6f4b --- /dev/null +++ b/docs/library/forms/form-ll.md @@ -0,0 +1,464 @@ +--- +components: + - rx.form.root + - rx.form.field + - rx.form.control + - rx.form.label + - rx.form.message + - rx.form.submit + +FormRoot: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email"), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + name="email", + ), + **props, + ) + +FormField: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email", match="typeMismatch"), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + **props, + ), + reset_on_submit=True, + ) + +FormMessage: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email", **props,), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + name="email", + ), + on_submit=lambda form_data: rx.window_alert(form_data.to_string()), + reset_on_submit=True, + ) +--- + +# Form + +```python exec +import reflex as rx +import reflex.components.radix.primitives as rdxp +``` + +```md warning info +# Low Level Form is Experimental + +Please use the High Level Form for now for production. +``` + +Forms are used to collect information from your users. Forms group the inputs and submit them together. + +## Basic Example + +Here is an example of a form collecting an email address, with built-in validation on the email. If email entered is invalid, the form cannot be submitted. Note that the `form.submit` button is not automatically disabled. It is still clickable, but does not submit the form data. After successful submission, an alert window shows up and the form is cleared. There are a few `flex` containers used in the example to control the layout of the form components. + +```python demo +rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email", match="typeMismatch"), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + name="email", + ), + on_submit=lambda form_data: rx.window_alert(form_data.to_string()), + reset_on_submit=True, +) +``` + +In this example, the `rx.input` has an attribute `type="email"` and the `form.message` has the attribute `match="typeMismatch"`. Those are required for the form to validate the input by its type. The prop `as_child="True"` is required when using other components to construct a Form component. This example has used `rx.input` to construct the Form Control and `button` the Form Submit. + +## Form Anatomy + +```python eval +rx._x.code_block( + """form.root( + form.field( + form.label(...), + form.control(...), + form.message(...), + ), + form.submit(...), +)""", + language="python", +) +``` + +A Form Root (`form.root`) contains all the parts of a form. The Form Field (`form.field`), Form Submit (`form.submit`), etc should all be inside a Form Root. A Form Field can contain a Form Label (`form.label`), a Form Control (`form.control`), and a Form Message (`form.message`). A Form Label is a label element. A Form Control is where the user enters the input or makes selections. By default, the Form Control is a input. Using other form components to construct the Form Control is supported. To do that, set the prop `as_child=True` on the Form Control. + +```md alert info +The current version of Radix Forms does not support composing **Form Control** with other Radix form primitives such as **Checkbox**, **Select**, etc. +``` + +The Form Message is a validation message which is automatically wired (functionality and accessibility). When the Form Control determines the input is invalid, the Form Message is shown. The `match` prop is to enable [client side validation](#client-side-validation). To perform [server side validation](#server-side-validation), **both** the `force_match` prop of the Form Control and the `server_invalid` prop of the Form Field are set together. + +The Form Submit is by default a button that submits the form. To use another button component as a Form Submit, include that button as a child inside `form.submit` and set the prop `as_child=True`. + +The `on_submit` prop of the Form Root accepts an event handler. It is called with the submitted form data dictionary. To clear the form after submission, set the `reset_on_submit=True` prop. + +## Data Submission + +As previously mentioned, the various pieces of data in the form are submitted together as a dictionary. The form control or the input components must have the `name` attribute. This `name` is the key to get the value from the form data dictionary. If no validation is needed, the form type components such as Checkbox, Radio Groups, TextArea can be included directly under the Form Root instead of inside a Form Control. + +```python demo exec +import reflex as rx +import reflex.components.radix.primitives as rdxp + +class RadixFormSubmissionState(rx.State): + form_data: dict + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + @rx.var + def form_data_keys(self) -> list: + return list(self.form_data.keys()) + + @rx.var + def form_data_values(self) -> list: + return list(self.form_data.values()) + + +def radix_form_submission_example(): + return rx.flex( + rx.form.root( + rx.flex( + rx.flex( + rx.checkbox( + default_checked=True, + name="box1", + ), + rx.text("box1 checkbox"), + direction="row", + spacing="2", + align="center", + ), + rx.radio.root( + rx.flex( + rx.radio.item(value="1"), + "1", + direction="row", + align="center", + spacing="2", + ), + rx.flex( + rx.radio.item(value="2"), + "2", + direction="row", + align="center", + spacing="2", + ), + rx.flex( + rx.radio.item(value="3"), + "3", + direction="row", + align="center", + spacing="2", + ), + default_value="1", + name="box2", + ), + rx.input( + placeholder="box3 textfield input", + name="box3", + ), + rx.select.root( + rx.select.trigger( + placeholder="box4 select", + ), + rx.select.content( + rx.select.group( + rx.select.item( + "Orange", + value="orange" + ), + rx.select.item( + "Apple", + value="apple" + ), + ), + ), + name="box4", + ), + rx.flex( + rx.switch( + default_checked=True, + name="box5", + ), + "box5 switch", + spacing="2", + align="center", + direction="row", + ), + rx.flex( + rx.slider( + default_value=[40], + width="100%", + name="box6", + ), + "box6 slider", + direction="row", + spacing="2", + align="center", + ), + rx.text_area( + placeholder="Enter for box7 textarea", + name="box7", + ), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="4", + ), + on_submit=RadixFormSubmissionState.handle_submit, + ), + rx.divider(size="4"), + rx.text( + "Results", + weight="bold", + ), + rx.foreach(RadixFormSubmissionState.form_data_keys, + lambda key, idx: rx.text(key, " : ", RadixFormSubmissionState.form_data_values[idx]) + ), + direction="column", + spacing="4", + ) +``` + +## Validation + +Server side validation is done through **Computed Vars** on the State. The **Var** should return a boolean flag indicating when input is invalid. Set that **Var** on both the `server_invalid` prop of `form.field` and the `force_match` prop of `form.message`. There is an example how to do that in the [Final Example](#final-example). + +## Final Example + +The final example shows a form that collects username and email during sign-up and validates them using server side validation. When server side validation fails, messages are displayed in red to show what is not accepted in the form, and the submit button is disabled. After submission, the collected form data is displayed in texts below the form and the form is cleared. + +```python demo exec +import re +import reflex as rx +import reflex.components.radix.primitives as rdxp + +class RadixFormState(rx.State): + # These track the user input real time for validation + user_entered_username: str + user_entered_email: str + + # These are the submitted data + username: str + email: str + + mock_username_db: list[str] = ["reflex", "admin"] + + # Add explicit setters + def set_user_entered_username(self, value: str): + self.user_entered_username = value + + def set_user_entered_email(self, value: str): + self.user_entered_email = value + + def set_username(self, value: str): + self.username = value + + def set_email(self, value: str): + self.email = value + + @rx.var + def invalid_email(self) -> bool: + return not re.match(r"[^@]+@[^@]+\.[^@]+", self.user_entered_email) + + @rx.var + def username_empty(self) -> bool: + return not self.user_entered_username.strip() + + @rx.var + def username_is_taken(self) -> bool: + return self.user_entered_username in self.mock_username_db + + @rx.var + def input_invalid(self) -> bool: + return self.invalid_email or self.username_is_taken or self.username_empty + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.username = form_data.get("username") + self.email = form_data.get("email") + +def radix_form_example(): + return rx.flex( + rx.form.root( + rx.flex( + rx.form.field( + rx.flex( + rx.form.label("Username"), + rx.form.control( + rx.input( + placeholder="Username", + # workaround: `name` seems to be required when on_change is set + on_change=RadixFormState.set_user_entered_username, + name="username", + ), + as_child=True, + ), + # server side validation message can be displayed inside a rx.cond + rx.cond( + RadixFormState.username_empty, + rx.form.message( + "Username cannot be empty", + color="var(--red-11)", + ), + ), + # server side validation message can be displayed by `force_match` prop + rx.form.message( + "Username already taken", + # this is a workaround: + # `force_match` does not work without `match` + # This case does not want client side validation + # and intentionally not set `required` on the input + # so "valueMissing" is always false + match="valueMissing", + force_match=RadixFormState.username_is_taken, + color="var(--red-11)", + ), + direction="column", + spacing="2", + align="stretch", + ), + name="username", + server_invalid=RadixFormState.username_is_taken, + ), + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + on_change=RadixFormState.set_user_entered_email, + name="email", + ), + as_child=True, + ), + rx.form.message( + "A valid Email is required", + match="valueMissing", + force_match=RadixFormState.invalid_email, + color="var(--red-11)", + ), + direction="column", + spacing="2", + align="stretch", + ), + name="email", + server_invalid=RadixFormState.invalid_email, + ), + rx.form.submit( + rx.button( + "Submit", + disabled=RadixFormState.input_invalid, + ), + as_child=True, + ), + direction="column", + spacing="4", + width="25em", + ), + on_submit=RadixFormState.handle_submit, + reset_on_submit=True, + ), + rx.divider(size="4"), + rx.text( + "Username submitted: ", + rx.text( + RadixFormState.username, + weight="bold", + color="var(--accent-11)", + ), + ), + rx.text( + "Email submitted: ", + rx.text( + RadixFormState.email, + weight="bold", + color="var(--accent-11)", + ), + ), + direction="column", + spacing="4", + ) +``` diff --git a/docs/library/forms/form.md b/docs/library/forms/form.md new file mode 100644 index 00000000000..bc771b7559f --- /dev/null +++ b/docs/library/forms/form.md @@ -0,0 +1,239 @@ +--- +components: + - rx.form + - rx.form.root + - rx.form.field + - rx.form.control + - rx.form.label + - rx.form.message + - rx.form.submit + +FormRoot: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email"), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + name="email", + ), + **props, + ) + +FormField: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email", match="typeMismatch"), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + **props, + ), + reset_on_submit=True, + ) + +FormLabel: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email", **props,), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email", match="typeMismatch"), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + ), + reset_on_submit=True, + ) + +FormMessage: | + lambda **props: rx.form.root( + rx.form.field( + rx.flex( + rx.form.label("Email"), + rx.form.control( + rx.input( + placeholder="Email Address", + # type attribute is required for "typeMismatch" validation + type="email", + ), + as_child=True, + ), + rx.form.message("Please enter a valid email", **props,), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + align="stretch", + ), + name="email", + ), + on_submit=lambda form_data: rx.window_alert(form_data.to_string()), + reset_on_submit=True, + ) +--- + +```python exec +import reflex as rx +``` + +# Form + +Forms are used to collect user input. The `rx.form` component is used to group inputs and submit them together. + +The form component's children can be form controls such as `rx.input`, `rx.checkbox`, `rx.slider`, `rx.textarea`, `rx.radio_group`, `rx.select` or `rx.switch`. The controls should have a `name` attribute that is used to identify the control in the form data. The `on_submit` event trigger submits the form data as a dictionary to the `handle_submit` event handler. + +The form is submitted when the user clicks the submit button or presses enter on the form controls. + +```python demo exec +class FormState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def form_example(): + return rx.vstack( + rx.form( + rx.vstack( + rx.input(placeholder="First Name", name="first_name"), + rx.input(placeholder="Last Name", name="last_name"), + rx.hstack( + rx.checkbox("Checked", name="check"), + rx.switch("Switched", name="switch"), + ), + rx.button("Submit", type="submit"), + ), + on_submit=FormState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.heading("Results"), + rx.text(FormState.form_data.to_string()), + ) +``` + +```md alert warning +# When using the form you must include a button or input with `type='submit'`. +``` + +```md alert info +# Using `name` vs `id`. + +When using the `name` attribute in form controls like `rx.switch`, `rx.radio_group`, and `rx.checkbox`, these controls will only be included in the form data if their values are set (e.g., if the checkbox is checked, the switch is toggled, or a radio option is selected). + +If you need these controls to be passed in the form data even when their values are not set, you can use the `id` attribute instead of name. The id attribute ensures that the control is always included in the submitted form data, regardless of whether its value is set or not. +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=5287&end=6040 +# Video: Forms +``` + +## Dynamic Forms + +Forms can be dynamically created by iterating through state vars using `rx.foreach`. + +This example allows the user to add new fields to the form prior to submit, and all +fields will be included in the form data passed to the `handle_submit` function. + +```python demo exec +class DynamicFormState(rx.State): + form_data: dict = {} + form_fields: list[str] = ["first_name", "last_name", "email"] + + @rx.var(cache=True) + def form_field_placeholders(self) -> list[str]: + return [ + " ".join(w.capitalize() for w in field.split("_")) + for field in self.form_fields + ] + + @rx.event + def add_field(self, form_data: dict): + new_field = form_data.get("new_field") + if not new_field: + return + field_name = new_field.strip().lower().replace(" ", "_") + self.form_fields.append(field_name) + + @rx.event + def handle_submit(self, form_data: dict): + self.form_data = form_data + + +def dynamic_form(): + return rx.vstack( + rx.form( + rx.vstack( + rx.foreach( + DynamicFormState.form_fields, + lambda field, idx: rx.input( + placeholder=DynamicFormState.form_field_placeholders[idx], + name=field, + ), + ), + rx.button("Submit", type="submit"), + ), + on_submit=DynamicFormState.handle_submit, + reset_on_submit=True, + ), + rx.form( + rx.hstack( + rx.input(placeholder="New Field", name="new_field"), + rx.button("+", type="submit"), + ), + on_submit=DynamicFormState.add_field, + reset_on_submit=True, + ), + rx.divider(), + rx.heading("Results"), + rx.text(DynamicFormState.form_data.to_string()), + ) +``` diff --git a/docs/library/forms/input-ll.md b/docs/library/forms/input-ll.md new file mode 100644 index 00000000000..1604614d44b --- /dev/null +++ b/docs/library/forms/input-ll.md @@ -0,0 +1,127 @@ +--- +components: + - rx.input + - rx.input.slot +--- + +```python exec +import reflex as rx +``` + +# Input + +A text field is an input field that users can type into. This component uses Radix's [text field](https://www.radix-ui.com/themes/docs/components/text-field) component. + +## Overview + +The TextField component is used to capture user input and can include an optional slot for buttons and icons. It is based on the
element and supports common margin props. + +## Basic Example + +```python demo +rx.input( + rx.input.slot( + rx.icon(tag="search"), + + ), + placeholder="Search here...", +) +``` + +## Stateful Example with Blur Event + +```python demo exec +class TextfieldBlur1(rx.State): + text: str = "Hello World!" + + @rx.event + def set_text(self, text: str): + self.text = text + +def blur_example1(): + return rx.vstack( + rx.heading(TextfieldBlur1.text), + rx.input( + rx.input.slot( + rx.icon(tag="search"), + ), + placeholder="Search here...", + on_blur=TextfieldBlur1.set_text, + ) + ) +``` + +## Controlled Example + +```python demo exec +class TextfieldControlled1(rx.State): + text: str = "Hello World!" + + @rx.event + def set_text(self, text: str): + self.text = text + +def controlled_example1(): + return rx.vstack( + rx.heading(TextfieldControlled1.text), + rx.input( + rx.input.slot( + rx.icon(tag="search"), + ), + placeholder="Search here...", + value=TextfieldControlled1.text, + on_change=TextfieldControlled1.set_text, + ), + ) +``` + +# Real World Example + +```python demo exec + +def song(title, initials: str, genre: str): + return rx.card(rx.flex( + rx.flex( + rx.avatar(fallback=initials), + rx.flex( + rx.text(title, size="2", weight="bold"), + rx.text(genre, size="1", color_scheme="gray"), + direction="column", + spacing="1", + ), + direction="row", + align_items="left", + spacing="1", + ), + rx.flex( + rx.icon(tag="chevron_right"), + align_items="center", + ), + justify="between", + )) + +def search(): + return rx.card( + rx.flex( + rx.input( + rx.input.slot( + rx.icon(tag="search"), + ), + placeholder="Search songs...", + ), + rx.flex( + song("The Less I Know", "T", "Rock"), + song("Breathe Deeper", "ZB", "Rock"), + song("Let It Happen", "TF", "Rock"), + song("Borderline", "ZB", "Pop"), + song("Lost In Yesterday", "TO", "Rock"), + song("Is It True", "TO", "Rock"), + direction="column", + spacing="1", + ), + direction="column", + spacing="3", + ), + style={"maxWidth": 500}, +) +``` diff --git a/docs/library/forms/input.md b/docs/library/forms/input.md new file mode 100644 index 00000000000..bd41b0a6688 --- /dev/null +++ b/docs/library/forms/input.md @@ -0,0 +1,164 @@ +--- +components: + - rx.input + - rx.input.slot + +Input: | + lambda **props: rx.input(placeholder="Search the docs", **props) + +TextFieldSlot: | + lambda **props: rx.input( + rx.input.slot( + rx.icon(tag="search", height="16", width="16"), + **props, + ), + placeholder="Search the docs", + ) +--- + +```python exec +import reflex as rx +from pcweb.pages.docs import library +``` + +# Input + +The `input` component is an input field that users can type into. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=1517&end=1869 +# Video: Input +``` + +## Basic Example + +The `on_blur` event handler is called when focus has left the `input` for example, it’s called when the user clicks outside of a focused text input. + +```python demo exec +class TextfieldBlur(rx.State): + text: str = "Hello World!" + + @rx.event + def set_text(self, value: str): + self.text = value + +def blur_example(): + return rx.vstack( + rx.heading(TextfieldBlur.text), + rx.input( + placeholder="Search here...", + on_blur=TextfieldBlur.set_text, + ), + ) +``` + +The `on_change` event handler is called when the `value` of `input` has changed. + +```python demo exec +class TextfieldControlled(rx.State): + text: str = "Hello World!" + + @rx.event + def set_text(self, value: str): + self.text = value + +def controlled_example(): + return rx.vstack( + rx.heading(TextfieldControlled.text), + rx.input( + placeholder="Search here...", + value=TextfieldControlled.text, + on_change=TextfieldControlled.set_text, + ), + ) +``` + +Behind the scenes, the input component is implemented as a debounced input to avoid sending individual state updates per character to the backend while the user is still typing. This allows a state variable to directly control the `value` prop from the backend without the user experiencing input lag. + +## Input Types + +The `type` prop controls how the input is rendered (e.g. plain text, password, file picker). + +It accepts the same values as the native HTML `` attribute, such as: + +- `"text"` (default) +- `"password"` +- `"email"` +- `"number"` +- `"file"` +- `"checkbox"` +- `"radio"` +- `"date"` +- `"time"` +- `"url"` +- `"color"` + +and several others. See the [MDN reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types) for the full list. + +```python demo +rx.vstack( + rx.input(placeholder="Username", type="text"), + rx.input(placeholder="Password", type="password"), + rx.input(type="date"), +) +``` + +## Submitting a form using input + +The `name` prop is needed to submit with its owning form as part of a name/value pair. + +When the `required` prop is `True`, it indicates that the user must input text before the owning form can be submitted. + +The `type` is set here to `password`. The element is presented as a one-line plain text editor control in which the text is obscured so that it cannot be read. The `type` prop can take any value of `email`, `file`, `password`, `text` and several others. Learn more [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). + +```python demo exec +class FormInputState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def form_input1(): + return rx.card( + rx.vstack( + rx.heading("Example Form"), + rx.form.root( + rx.hstack( + rx.input( + name="input", + placeholder="Enter text...", + type="text", + required=True, + ), + rx.button("Submit", type="submit"), + width="100%", + ), + on_submit=FormInputState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.hstack( + rx.heading("Results:"), + rx.badge(FormInputState.form_data.to_string()), + ), + align_items="left", + width="100%", + ), + width="50%", + ) +``` + +To learn more about how to use forms in the [Form]({library.forms.form.path}) docs. + +## Setting a value without using a State var + +Set the value of the specified reference element, without needing to link it up to a State var. This is an alternate way to modify the value of the `input`. + +```python demo +rx.hstack( + rx.input(id="input1"), + rx.button("Erase", on_click=rx.set_value("input1", "")), +) +``` diff --git a/docs/library/forms/radio_group.md b/docs/library/forms/radio_group.md new file mode 100644 index 00000000000..e0c62187d9a --- /dev/null +++ b/docs/library/forms/radio_group.md @@ -0,0 +1,101 @@ +--- +components: + - rx.radio_group + - rx.radio_group.root + - rx.radio_group.item + +HighLevelRadioGroup: | + lambda **props: rx.radio_group(["1", "2", "3", "4", "5"], **props) + +RadioGroupRoot: | + lambda **props: rx.radio_group.root( + rx.radio_group.item(value="1"), + rx.radio_group.item(value="2"), + rx.radio_group.item(value="3"), + rx.radio_group.item(value="4"), + rx.radio_group.item(value="5"), + **props + ) + +RadioGroupItem: | + lambda **props: rx.radio_group.root( + rx.radio_group.item(value="1", **props), + rx.radio_group.item(value="2", **props), + rx.radio_group.item(value="3",), + rx.radio_group.item(value="4",), + rx.radio_group.item(value="5",), + ) +--- + +```python exec +import reflex as rx +from pcweb.templates.docpage import style_grid +``` + +# Radio Group + +A set of interactive radio buttons where only one can be selected at a time. + +## Basic example + +```python demo exec +class RadioGroupState(rx.State): + item: str = "No Selection" + + @rx.event + def set_item(self, item: str): + self.item = item + +def radio_group_state_example(): + return rx.vstack( + rx.badge(RadioGroupState.item, color_scheme="green"), + rx.radio(["1", "2", "3"], on_change=RadioGroupState.set_item, direction="row"), + ) +``` + +## Submitting a form using Radio Group + +The `name` prop is used to name the group. It is submitted with its owning form as part of a name/value pair. + +When the `required` prop is `True`, it indicates that the user must check a radio item before the owning form can be submitted. + +```python demo exec +class FormRadioState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def radio_form_example(): + return rx.card( + rx.vstack( + rx.heading("Example Form"), + rx.form.root( + rx.vstack( + rx.radio_group( + ["Option 1", "Option 2", "Option 3"], + name="radio_choice", + direction="row", + ), + rx.button("Submit", type="submit"), + width="100%", + spacing="4", + ), + on_submit=FormRadioState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.hstack( + rx.heading("Results:"), + rx.badge(FormRadioState.form_data.to_string()), + ), + align_items="left", + width="100%", + spacing="4", + ), + width="50%", + ) +``` diff --git a/docs/library/forms/select-ll.md b/docs/library/forms/select-ll.md new file mode 100644 index 00000000000..50b7ad26ef3 --- /dev/null +++ b/docs/library/forms/select-ll.md @@ -0,0 +1,305 @@ +--- +components: + - rx.select + - rx.select.root + - rx.select.trigger + - rx.select.content + - rx.select.group + - rx.select.item + - rx.select.label + - rx.select.separator +--- + +```python exec +import random +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +import reflex.components.radix.primitives as rdxp +from pcweb.templates.docpage import style_grid +``` + +# Select + +Displays a list of options for the user to pick from, triggered by a button. + +## Basic Example + +```python demo +rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.label("Fruits"), + rx.select.item("Orange", value="orange"), + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape", disabled=True), + ), + rx.select.separator(), + rx.select.group( + rx.select.label("Vegetables"), + rx.select.item("Carrot", value="carrot"), + rx.select.item("Potato", value="potato"), + ), + ), + default_value="apple", +) +``` + +## Usage + +## Disabling + +It is possible to disable individual items in a `select` using the `disabled` prop associated with the `rx.select.item`. + +```python demo +rx.select.root( + rx.select.trigger(placeholder="Select a Fruit"), + rx.select.content( + rx.select.group( + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape", disabled=True), + rx.select.item("Pineapple", value="pineapple"), + ), + ), +) +``` + +To prevent the user from interacting with select entirely, set the `disabled` prop to `True` on the `rx.select.root` component. + +```python demo +rx.select.root( + rx.select.trigger(placeholder="This is Disabled"), + rx.select.content( + rx.select.group( + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape"), + ), + ), + disabled=True, +) +``` + +## Setting Defaults + +It is possible to set several default values when constructing a `select`. + +The `placeholder` prop in the `rx.select.trigger` specifies the content that will be rendered when `value` or `default_value` is empty or not set. + +```python demo +rx.select.root( + rx.select.trigger(placeholder="pick a fruit"), + rx.select.content( + rx.select.group( + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape"), + ), + ), +) +``` + +The `default_value` in the `rx.select.root` specifies the value of the `select` when initially rendered. +The `default_value` should correspond to the `value` of a child `rx.select.item`. + +```python demo +rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape"), + ), + ), + default_value="apple", +) +``` + +## Fully controlled + +The `on_change` event trigger is fired when the value of the select changes. +In this example the `rx.select_root` `value` prop specifies which item is selected, and this +can also be controlled using state and a button without direct interaction with the select component. + +```python demo exec +class SelectState2(rx.State): + + values: list[str] = ["apple", "grape", "pear"] + + value: str = "" + + def set_value(self, value: str): + self.value = value + + @rx.event + def choose_randomly(self): + """Change the select value var.""" + original_value = self.value + while self.value == original_value: + self.value = random.choice(self.values) + + +def select_example2(): + return rx.vstack( + rx.select.root( + rx.select.trigger(placeholder="No Selection"), + rx.select.content( + rx.select.group( + rx.foreach(SelectState2.values, lambda x: rx.select.item(x, value=x)) + ), + ), + value=SelectState2.value, + on_change=SelectState2.set_value, + + ), + rx.button("Choose Randomly", on_click=SelectState2.choose_randomly), + rx.button("Reset", on_click=SelectState2.set_value("")), + ) +``` + +The `open` prop and `on_open_change` event trigger work similarly to `value` and `on_change` to control the open state of the select. +If `on_open_change` handler does not alter the `open` prop, the select will not be able to be opened or closed by clicking on the +`select_trigger`. + +```python demo exec +class SelectState8(rx.State): + is_open: bool = False + + @rx.event + def set_is_open(self, value: bool): + self.is_open = value + +def select_example8(): + return rx.flex( + rx.select.root( + rx.select.trigger(placeholder="No Selection"), + rx.select.content( + rx.select.group( + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape"), + ), + ), + open=SelectState8.is_open, + on_open_change=SelectState8.set_is_open, + ), + rx.button("Toggle", on_click=SelectState8.set_is_open(~SelectState8.is_open)), + spacing="2", + ) +``` + +### Submitting a Form with Select + +When a select is part of a form, the `name` prop of the `rx.select.root` sets the key that will be submitted with the form data. + +The `value` prop of `rx.select.item` provides the value to be associated with the `name` key when the form is submitted with that item selected. + +When the `required` prop of the `rx.select.root` is `True`, it indicates that the user must select a value before the form may be submitted. + +```python demo exec +class FormSelectState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def form_select(): + return rx.flex( + rx.form.root( + rx.flex( + rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.label("Fruits"), + rx.select.item("Orange", value="orange"), + rx.select.item("Apple", value="apple"), + rx.select.item("Grape", value="grape"), + ), + rx.select.separator(), + rx.select.group( + rx.select.label("Vegetables"), + rx.select.item("Carrot", value="carrot"), + rx.select.item("Potato", value="potato"), + ), + ), + default_value="apple", + name="select", + ), + rx.button("Submit"), + width="100%", + direction="column", + spacing="2", + ), + on_submit=FormSelectState.handle_submit, + reset_on_submit=True, + ), + rx.divider(size="4"), + rx.heading("Results"), + rx.text(FormSelectState.form_data.to_string()), + width="100%", + direction="column", + spacing="2", + ) +``` + +## Real World Example + +```python demo +rx.card( + rx.flex( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/reflex_banner.png", width="100%", height="auto"), + rx.flex( + rx.heading("Reflex Swag", size="4", margin_bottom="4px"), + rx.heading("$99", size="6", margin_bottom="4px"), + direction="row", justify="between", + width="100%", + ), + rx.text("Reflex swag with a sense of nostalgia, as if they carry whispered tales of past adventures", size="2", margin_bottom="4px"), + rx.divider(size="4"), + rx.flex( + rx.flex( + rx.text("Color", size="2", margin_bottom="4px", color_scheme="gray"), + rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.item("Light", value="light"), + rx.select.item("Dark", value="dark"), + ), + ), + default_value="light", + ), + direction="column", + ), + rx.flex( + rx.text("Size", size="2", margin_bottom="4px", color_scheme="gray"), + rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.item("24", value="24"), + rx.select.item("26", value="26"), + rx.select.item("28", value="28", disabled=True), + rx.select.item("30", value="30"), + rx.select.item("32", value="32"), + rx.select.item("34", value="34"), + rx.select.item("36", value="36"), + ), + ), + default_value="30", + ), + direction="column", + ), + rx.button(rx.icon(tag="plus"), "Add"), + align="end", + justify="between", + spacing="2", + width="100%", + ), + width="15em", + direction="column", + spacing="2", + ), +) +``` diff --git a/docs/library/forms/select.md b/docs/library/forms/select.md new file mode 100644 index 00000000000..b37e7104880 --- /dev/null +++ b/docs/library/forms/select.md @@ -0,0 +1,202 @@ +--- +components: + - rx.select + - rx.select.root + - rx.select.trigger + - rx.select.content + - rx.select.group + - rx.select.item + - rx.select.label + - rx.select.separator + +HighLevelSelect: | + lambda **props: rx.select(["apple", "grape", "pear"], default_value="pear", **props) + +SelectRoot: | + lambda **props: rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.item("apple", value="apple"), + rx.select.item("grape", value="grape"), + rx.select.item("pear", value="pear"), + ), + ), + default_value="pear", + **props + ) + +SelectTrigger: | + lambda **props: rx.select.root( + rx.select.trigger(**props), + rx.select.content( + rx.select.group( + rx.select.item("apple", value="apple"), + rx.select.item("grape", value="grape"), + rx.select.item("pear", value="pear"), + ), + ), + default_value="pear", + ) + +SelectContent: | + lambda **props: rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.item("apple", value="apple"), + rx.select.item("grape", value="grape"), + rx.select.item("pear", value="pear"), + ), + **props, + ), + default_value="pear", + ) + +SelectItem: | + lambda **props: rx.select.root( + rx.select.trigger(), + rx.select.content( + rx.select.group( + rx.select.item("apple", value="apple", **props), + rx.select.item("grape", value="grape"), + rx.select.item("pear", value="pear"), + ), + ), + default_value="pear", + ) +--- + +```python exec +import random +import reflex as rx +from pcweb.templates.docpage import style_grid +``` + +# Select + +Displays a list of options for the user to pick from—triggered by a button. + +```python demo exec +class SelectState(rx.State): + value: str = "apple" + + @rx.event + def change_value(self, value: str): + """Change the select value var.""" + self.value = value + + +def select_intro(): + return rx.center( + rx.select( + ["apple", "grape", "pear"], + value=SelectState.value, + on_change=SelectState.change_value, + ), + rx.badge(SelectState.value), + ) +``` + +```python demo exec +class SelectState3(rx.State): + + values: list[str] = ["apple", "grape", "pear"] + + value: str = "apple" + + def set_value(self, value: str): + self.value = value + + @rx.event + def change_value(self): + """Change the select value var.""" + self.value = random.choice(self.values) + + +def select_example3(): + return rx.vstack( + rx.select( + SelectState3.values, + value=SelectState3.value, + on_change=SelectState3.set_value, + ), + rx.button("Change Value", on_click=SelectState3.change_value), + + ) +``` + +The `on_open_change` event handler acts in a similar way to the `on_change` and is called when the open state of the select changes. + +```python demo +rx.select( + ["apple", "grape", "pear"], + on_change=rx.window_alert("on_change event handler called"), +) + +``` + +### Submitting a form using select + +The `name` prop is needed to submit with its owning form as part of a name/value pair. + +When the `required` prop is `True`, it indicates that the user must select a value before the owning form can be submitted. + +```python demo exec +class FormSelectState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def select_form_example(): + return rx.card( + rx.vstack( + rx.heading("Example Form"), + rx.form.root( + rx.flex( + rx.select(["apple", "grape", "pear"], default_value="apple", name="select", required=True), + rx.button("Submit", flex="1", type="submit"), + width="100%", + spacing="3", + ), + on_submit=FormSelectState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.hstack( + rx.heading("Results:"), + rx.badge(FormSelectState.form_data.to_string()), + ), + align_items="left", + width="100%", + ), + width="50%", + ) +``` + +### Using Select within a Drawer component + +If using within a [Drawer](/docs/library/overlay/drawer) component, set the `position` prop to `"popper"` to ensure the select menu is displayed correctly. + +```python demo +rx.drawer.root( + rx.drawer.trigger(rx.button("Open Drawer")), + rx.drawer.overlay(z_index="5"), + rx.drawer.portal( + rx.drawer.content( + rx.vstack( + rx.drawer.close(rx.box(rx.button("Close"))), + rx.select(["apple", "grape", "pear"], position="popper"), + ), + width="20em", + padding="2em", + background_color=rx.color("gray", 1), + ), + ), + direction="left", +) +``` diff --git a/docs/library/forms/slider.md b/docs/library/forms/slider.md new file mode 100644 index 00000000000..cc537b224b6 --- /dev/null +++ b/docs/library/forms/slider.md @@ -0,0 +1,133 @@ +--- +components: + - rx.slider + +Slider: | + lambda **props: rx.center(rx.slider(default_value=40, height="100%", **props), height="4em", width="100%") +--- + +```python exec +import reflex as rx +from pcweb.templates.docpage import style_grid +``` + +# Slider + +Provides user selection from a range of values. The + +## Basic Example + +The slider can be controlled by a single value or a range of values. Slider can be hooked to state to control its value. Passing a list of two values creates a range slider. + +```python demo exec +class SliderState(rx.State): + value: int = 50 + + @rx.event + def set_end(self, value: list[int | float]): + self.value = value[0] + +def slider_intro(): + return rx.vstack( + rx.heading(SliderState.value), + rx.slider(on_value_commit=SliderState.set_end), + width="100%", + ) +``` + +## Range Slider + +Range slider is created by passing a list of two values to the `default_value` prop. The list should contain two values that are in the range of the slider. + +```python demo exec +class RangeSliderState(rx.State): + value_start: int = 25 + value_end: int = 75 + + @rx.event + def set_end(self, value: list[int | float]): + self.value_start = value[0] + self.value_end = value[1] + +def range_slider_intro(): + return rx.vstack( + rx.hstack( + rx.heading(RangeSliderState.value_start), + rx.heading(RangeSliderState.value_end), + ), + rx.slider( + default_value=[25, 75], + min_=0, + max=100, + size="1", + on_value_commit=RangeSliderState.set_end, + ), + width="100%", + ) +``` + +## Live Updating Slider + +You can use the `on_change` prop to update the slider value as you interact with it. The `on_change` prop takes a function that will be called with the new value of the slider. + +Here we use the `throttle` method to limit the rate at which the function is called, which is useful to prevent excessive updates. In this example, the slider value is updated every 100ms. + +```python demo exec +class LiveSliderState(rx.State): + value: int = 50 + + @rx.event + def set_end(self, value: list[int | float]): + self.value = value[0] + +def live_slider_intro(): + return rx.vstack( + rx.heading(LiveSliderState.value), + rx.slider( + default_value=50, + min_=0, + max=100, + on_change=LiveSliderState.set_end.throttle(100), + ), + width="100%", + ) +``` + +## Slider in forms + +Here we show how to use a slider in a form. We use the `name` prop to identify the slider in the form data. The form data is then passed to the `handle_submit` method to be processed. + +```python demo exec +class FormSliderState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def slider_form_example(): + return rx.card( + rx.vstack( + rx.heading("Example Form"), + rx.form.root( + rx.hstack( + rx.slider(default_value=40, name="slider"), + rx.button("Submit", type="submit"), + width="100%", + ), + on_submit=FormSliderState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.hstack( + rx.heading("Results:"), + rx.badge(FormSliderState.form_data.to_string()), + ), + align_items="left", + width="100%", + ), + width="50%", + ) +``` diff --git a/docs/library/forms/switch.md b/docs/library/forms/switch.md new file mode 100644 index 00000000000..8efc3e687ca --- /dev/null +++ b/docs/library/forms/switch.md @@ -0,0 +1,108 @@ +--- +components: + - rx.switch + +Switch: | + lambda **props: rx.switch(**props) +--- + +```python exec +import reflex as rx +from pcweb.templates.docpage import style_grid +from pcweb.pages.docs import vars +``` + +# Switch + +A toggle switch alternative to the checkbox. + +## Basic Example + +Here is a basic example of a switch. We use the `on_change` trigger to toggle the value in the state. + +```python demo exec +class SwitchState(rx.State): + value: bool = False + + @rx.event + def set_end(self, value: bool): + self.value = value + +def switch_intro(): + return rx.center( + rx.switch(on_change=SwitchState.set_end), + rx.badge(SwitchState.value), + ) +``` + +## Control the value + +The `checked` prop is used to control the state of the switch. The event `on_change` is called when the state of the switch changes, when the `change_checked` event handler is called. + +The `disabled` prop when `True`, prevents the user from interacting with the switch. In our example below, even though the second switch is `disabled` we are still able to change whether it is checked or not using the `checked` prop. + +```python demo exec +class ControlSwitchState(rx.State): + + checked = True + + @rx.event + def change_checked(self, checked: bool): + """Change the switch checked var.""" + self.checked = checked + + +def control_switch_example(): + return rx.hstack( + rx.switch( + checked=ControlSwitchState.checked, + on_change=ControlSwitchState.change_checked, + ), + rx.switch( + checked=ControlSwitchState.checked, + on_change=ControlSwitchState.change_checked, + disabled=True, + ), + ) +``` + +## Switch in forms + +The `name` of the switch is needed to submit with its owning form as part of a name/value pair. When the `required` prop is `True`, it indicates that the user must check the switch before the owning form can be submitted. + +The `value` prop is only used for form submission, use the `checked` prop to control state of the `switch`. + +```python demo exec +class FormSwitchState(rx.State): + form_data: dict = {} + + @rx.event + def handle_submit(self, form_data: dict): + """Handle the form submit.""" + self.form_data = form_data + + +def switch_form_example(): + return rx.card( + rx.vstack( + rx.heading("Example Form"), + rx.form.root( + rx.hstack( + rx.switch(name="switch"), + rx.button("Submit", type="submit"), + width="100%", + ), + on_submit=FormSwitchState.handle_submit, + reset_on_submit=True, + ), + rx.divider(), + rx.hstack( + rx.heading("Results:"), + rx.badge(FormSwitchState.form_data.to_string()), + ), + align_items="left", + width="100%", + ), + width="50%", + ) +``` diff --git a/docs/library/forms/text_area.md b/docs/library/forms/text_area.md new file mode 100644 index 00000000000..de4f1b6a198 --- /dev/null +++ b/docs/library/forms/text_area.md @@ -0,0 +1,88 @@ +--- +components: + - rx.text_area + +TextArea: | + lambda **props: rx.text_area(**props) +--- + +```python exec +import reflex as rx +``` + +# Text Area + +A text area is a multi-line text input field. + +## Basic Example + +The text area component can be controlled by a single value. The `on_blur` prop can be used to update the value when the text area loses focus. + +```python demo exec +class TextAreaBlur(rx.State): + text: str = "Hello World!" + + @rx.event + def set_text(self, text: str): + self.text = text + +def blur_example(): + return rx.vstack( + rx.heading(TextAreaBlur.text), + rx.text_area( + placeholder="Type here...", + on_blur=TextAreaBlur.set_text, + ), + ) +``` + +## Text Area in forms + +Here we show how to use a text area in a form. We use the `name` prop to identify the text area in the form data. The form data is then passed to the `submit_feedback` method to be processed. + +```python demo exec +class TextAreaFeedbackState(rx.State): + feedback: str = "" + submitted: bool = False + + @rx.event + def set_feedback(self, value: str): + self.feedback = value + + @rx.event + def submit_feedback(self, form_data: dict): + self.submitted = True + + @rx.event + def reset_form(self): + self.feedback = "" + self.submitted = False + +def feedback_form(): + return rx.cond( + TextAreaFeedbackState.submitted, + rx.card( + rx.vstack( + rx.text("Thank you for your feedback!"), + rx.button("Submit another response", on_click=TextAreaFeedbackState.reset_form), + ), + ), + rx.card( + rx.form( + rx.flex( + rx.text("Are you enjoying Reflex?"), + rx.text_area( + placeholder="Write your feedback…", + value=TextAreaFeedbackState.feedback, + on_change=TextAreaFeedbackState.set_feedback, + resize="vertical", + ), + rx.button("Send", type="submit"), + direction="column", + spacing="3", + ), + on_submit=TextAreaFeedbackState.submit_feedback, + ), + ), + ) +``` diff --git a/docs/library/forms/upload.md b/docs/library/forms/upload.md new file mode 100644 index 00000000000..767122e5e4c --- /dev/null +++ b/docs/library/forms/upload.md @@ -0,0 +1,525 @@ +--- +components: + - rx.upload + - rx.upload.root + +Upload: | + lambda **props: rx.center(rx.upload(id="my_upload", **props)) +--- + +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +``` + +# File Upload + +Reflex makes it simple to add file upload functionality to your app. You can let users select files, store them on your server, and display or process them as needed. Below is a minimal example that demonstrates how to upload files, save them to disk, and display uploaded images using application state. + +## Basic File Upload Example + +You can let users upload files and keep track of them in your app’s state. The example below allows users to upload files, saves them using the backend, and then displays the uploaded files as images. + +```python +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +class State(rx.State): + uploaded_files: list[str] = [] + + @rx.event + async def handle_upload(self, files: list[rx.UploadFile]): + for file in files: + data = await file.read() + path = rx.get_upload_dir() / file.name + with path.open("wb") as f: + f.write(data) + self.uploaded_files.append(file.name) + +def upload_component(): + return rx.vstack( + rx.upload(id="upload"), + rx.button("Upload", on_click=State.handle_upload(rx.upload_files("upload"))), + rx.foreach(State.uploaded_files, lambda f: rx.image(src=rx.get_upload_url(f))), + ) +``` + +## How File Upload Works + +Selecting a file will add it to the browser file list, which can be rendered +on the frontend using the `rx.selected_files(id)` special Var. To clear the +selected files, you can use another special Var `rx.clear_selected_files(id)` as +an event handler. + +To upload the file(s), you need to bind an event handler and pass the special +`rx.upload_files(upload_id=id)` event arg to it. + +## File Storage Functions + +Reflex provides two key functions for handling uploaded files: + +### rx.get_upload_dir() + +- **Purpose**: Returns a `Path` object pointing to the server-side directory where uploaded files should be saved +- **Usage**: Used in backend event handlers to determine where to save uploaded files +- **Default Location**: `./uploaded_files` (can be customized via `REFLEX_UPLOADED_FILES_DIR` environment variable) +- **Type**: Returns `pathlib.Path` + +### rx.get_upload_url(filename) + +- **Purpose**: Returns the URL string that can be used in frontend components to access uploaded files +- **Usage**: Used in frontend components (like `rx.image`, `rx.video`) to display uploaded files +- **URL Format**: `/_upload/filename` +- **Type**: Returns `str` + +### Key Differences + +- **rx.get_upload_dir()** -> Backend file path for saving files +- **rx.get_upload_url()** -> Frontend URL for displaying files + +### Basic Upload Pattern + +Here is the standard pattern for handling file uploads: + +```python +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN + +def create_unique_filename(file_name: str): + import random + import string + + filename = "".join(random.choices(string.ascii_letters + string.digits, k=10)) + return filename + "_" + file_name + +class State(rx.State): + uploaded_files: list[str] = [] + + @rx.event + async def handle_upload(self, files: list[rx.UploadFile]): + """Handle file upload with proper directory management.""" + for file in files: + # Read the file data + upload_data = await file.read() + + # Get the upload directory (backend path) + upload_dir = rx.get_upload_dir() + + # Ensure the directory exists + upload_dir.mkdir(parents=True, exist_ok=True) + + # Create unique filename to prevent conflicts + unique_filename = create_unique_filename(file.name) + + # Create full file path + file_path = upload_dir / unique_filename + + # Save the file + with file_path.open("wb") as f: + f.write(upload_data) + + # Store filename for frontend display + self.uploaded_files.append(unique_filename) + +def upload_component(): + return rx.vstack( + rx.upload( + rx.text("Drop files here or click to select"), + id="file_upload", + border="2px dashed #ccc", + padding="2em", + ), + rx.button( + "Upload Files", + on_click=State.handle_upload(rx.upload_files(upload_id="file_upload")), + ), + # Display uploaded files using rx.get_upload_url() + rx.foreach( + State.uploaded_files, + lambda filename: rx.image(src=rx.get_upload_url(filename)) + ), + ) + +``` + +### Multiple File Upload + +Below is an example of how to allow multiple file uploads (in this case images). + +```python demo box +rx.image(src=f"{REFLEX_ASSETS_CDN}other/upload.gif") +``` + +```python +class State(rx.State): + """The app state.""" + + # The images to show. + img: list[str] + + @rx.event + async def handle_upload(self, files: list[rx.UploadFile]): + """Handle the upload of file(s). + + Args: + files: The uploaded files. + """ + for file in files: + upload_data = await file.read() + outfile = rx.get_upload_dir() / file.name + + # Save the file. + with outfile.open("wb") as file_object: + file_object.write(upload_data) + + # Update the img var. + self.img.append(file.name) + + +color = "rgb(107,99,246)" + + +def index(): + """The main view.""" + return rx.vstack( + rx.upload( + rx.vstack( + rx.button("Select File", color=color, bg="white", border=f"1px solid {color}"), + rx.text("Drag and drop files here or click to select files"), + ), + id="upload1", + border=f"1px dotted {color}", + padding="5em", + ), + rx.hstack(rx.foreach(rx.selected_files("upload1"), rx.text)), + rx.button( + "Upload", + on_click=State.handle_upload(rx.upload_files(upload_id="upload1")), + ), + rx.button( + "Clear", + on_click=rx.clear_selected_files("upload1"), + ), + rx.foreach(State.img, lambda img: rx.image(src=rx.get_upload_url(img))), + padding="5em", + ) +``` + +### Uploading a Single File (Video) + +Below is an example of how to allow only a single file upload and render (in this case a video). + +```python demo box +rx.el.video(src=f"{REFLEX_ASSETS_CDN}other/upload_single_video.webm", auto_play=True, controls=True, loop=True) +``` + +```python +class State(rx.State): + """The app state.""" + + # The video to show. + video: str + + @rx.event + async def handle_upload( + self, files: list[rx.UploadFile] + ): + """Handle the upload of file(s). + + Args: + files: The uploaded files. + """ + current_file = files[0] + upload_data = await current_file.read() + outfile = rx.get_upload_dir() / current_file.name + + # Save the file. + with outfile.open("wb") as file_object: + file_object.write(upload_data) + + # Update the video var. + self.video = current_file.name + + +color = "rgb(107,99,246)" + + +def index(): + """The main view.""" + return rx.vstack( + rx.upload( + rx.vstack( + rx.button( + "Select File", + color=color, + bg="white", + border=f"1px solid \{color}", + ), + rx.text( + "Drag and drop files here or click to select files" + ), + ), + id="upload1", + max_files=1, + border=f"1px dotted {color}", + padding="5em", + ), + rx.text(rx.selected_files("upload1")), + rx.button( + "Upload", + on_click=State.handle_upload( + rx.upload_files(upload_id="upload1") + ), + ), + rx.button( + "Clear", + on_click=rx.clear_selected_files("upload1"), + ), + rx.cond( + State.video, + rx.video(src=rx.get_upload_url(State.video)), + ), + padding="5em", + ) +``` + +### Customizing the Upload + +In the example below, the upload component accepts a maximum number of 5 files of specific types. +It also disables the use of the space or enter key in uploading files. + +To use a one-step upload, bind the event handler to the `rx.upload` component's +`on_drop` trigger. + +```python +class State(rx.State): + """The app state.""" + + # The images to show. + img: list[str] + + async def handle_upload(self, files: list[rx.UploadFile]): + """Handle the upload of file(s). + + Args: + files: The uploaded files. + """ + for file in files: + upload_data = await file.read() + outfile = rx.get_upload_dir() / file.name + + # Save the file. + with outfile.open("wb") as file_object: + file_object.write(upload_data) + + # Update the img var. + self.img.append(file.name) + + +color = "rgb(107,99,246)" + + +def index(): + """The main view.""" + return rx.vstack( + rx.upload( + rx.vstack( + rx.button("Select File", color=color, bg="white", border=f"1px solid {color}"), + rx.text("Drag and drop files here or click to select files"), + ), + id="upload2", + multiple=True, + accept = { + "application/pdf": [".pdf"], + "image/png": [".png"], + "image/jpeg": [".jpg", ".jpeg"], + "image/gif": [".gif"], + "image/webp": [".webp"], + "text/html": [".html", ".htm"], + }, + max_files=5, + disabled=False, + no_keyboard=True, + on_drop=State.handle_upload(rx.upload_files(upload_id="upload2")), + border=f"1px dotted {color}", + padding="5em", + ), + rx.grid( + rx.foreach( + State.img, + lambda img: rx.vstack( + rx.image(src=rx.get_upload_url(img)), + rx.text(img), + ), + ), + columns="2", + spacing="1", + ), + padding="5em", + ) +``` + +### Unstyled Upload Component + +To use a completely unstyled upload component and apply your own customization, use `rx.upload.root` instead: + +```python demo +rx.upload.root( + rx.box( + rx.icon( + tag="cloud_upload", + style={"width": "3rem", "height": "3rem", "color": "#2563eb", "marginBottom": "0.75rem"}, + ), + rx.hstack( + rx.text( + "Click to upload", + style={"fontWeight": "bold", "color": "#1d4ed8"}, + ), + " or drag and drop", + style={"fontSize": "0.875rem", "color": "#4b5563"}, + ), + rx.text( + "SVG, PNG, JPG or GIF (MAX. 5MB)", + style={"fontSize": "0.75rem", "color": "#6b7280", "marginTop": "0.25rem"}, + ), + style={ + "display": "flex", + "flexDirection": "column", + "alignItems": "center", + "justifyContent": "center", + "padding": "1.5rem", + "textAlign": "center", + }, + ), + id="my_upload", + style={ + "maxWidth": "24rem", + "height": "16rem", + "borderWidth": "2px", + "borderStyle": "dashed", + "borderColor": "#60a5fa", + "borderRadius": "0.75rem", + "cursor": "pointer", + "transitionProperty": "background-color", + "transitionDuration": "0.2s", + "transitionTimingFunction": "ease-in-out", + "display": "flex", + "alignItems": "center", + "justifyContent": "center", + "boxShadow": "0 1px 2px rgba(0, 0, 0, 0.05)", + }, +) +``` + +## Handling the Upload + +Your event handler should be an async function that accepts a single argument, +`files: list[UploadFile]`, which will contain [FastAPI UploadFile](https://fastapi.tiangolo.com/tutorial/request-files) instances. +You can read the files and save them anywhere as shown in the example. + +In your UI, you can bind the event handler to a trigger, such as a button +`on_click` event or upload `on_drop` event, and pass in the files using +`rx.upload_files()`. + +### Saving the File + +By convention, Reflex provides the function `rx.get_upload_dir()` to get the directory where uploaded files may be saved. The upload dir comes from the environment variable `REFLEX_UPLOADED_FILES_DIR`, or `./uploaded_files` if not specified. + +The backend of your app will mount this uploaded files directory on `/_upload` without restriction. Any files uploaded via this mechanism will automatically be publicly accessible. To get the URL for a file inside the upload dir, use the `rx.get_upload_url(filename)` function in a frontend component. + +```md alert info +# When using the Reflex hosting service, the uploaded files directory is not persistent and will be cleared on every deployment. For persistent storage of uploaded files, it is recommended to use an external service, such as S3. +``` + +### Directory Structure and URLs + +By default, Reflex creates the following structure: + +```text +your_project/ +├── uploaded_files/ # rx.get_upload_dir() points here +│ ├── image1.png +│ ├── document.pdf +│ └── video.mp4 +└── ... +``` + +The files are automatically served at: + +- `/_upload/image1.png` ← `rx.get_upload_url("image1.png")` +- `/_upload/document.pdf` ← `rx.get_upload_url("document.pdf")` +- `/_upload/video.mp4` ← `rx.get_upload_url("video.mp4")` + +## Cancellation + +The `id` provided to the `rx.upload` component can be passed to the special event handler `rx.cancel_upload(id)` to stop uploading on demand. Cancellation can be triggered directly by a frontend event trigger, or it can be returned from a backend event handler. + +## Progress + +The `rx.upload_files` special event arg also accepts an `on_upload_progress` event trigger which will be fired about every second during the upload operation to report the progress of the upload. This can be used to update a progress bar or other UI elements to show the user the progress of the upload. + +```python +class UploadExample(rx.State): + uploading: bool = False + progress: int = 0 + total_bytes: int = 0 + + @rx.event + async def handle_upload(self, files: list[rx.UploadFile]): + for file in files: + self.total_bytes += len(await file.read()) + + @rx.event + def handle_upload_progress(self, progress: dict): + self.uploading = True + self.progress = round(progress["progress"] * 100) + if self.progress >= 100: + self.uploading = False + + @rx.event + def cancel_upload(self): + self.uploading = False + return rx.cancel_upload("upload3") + + +def upload_form(): + return rx.vstack( + rx.upload( + rx.text("Drag and drop files here or click to select files"), + id="upload3", + border="1px dotted rgb(107,99,246)", + padding="5em", + ), + rx.vstack(rx.foreach(rx.selected_files("upload3"), rx.text)), + rx.progress(value=UploadExample.progress, max=100), + rx.cond( + ~UploadExample.uploading, + rx.button( + "Upload", + on_click=UploadExample.handle_upload( + rx.upload_files( + upload_id="upload3", + on_upload_progress=UploadExample.handle_upload_progress, + ), + ), + ), + rx.button("Cancel", on_click=UploadExample.cancel_upload), + ), + rx.text("Total bytes uploaded: ", UploadExample.total_bytes), + align="center", + ) +``` + +The `progress` dictionary contains the following keys: + +```javascript +\{ + 'loaded': 36044800, + 'total': 54361908, + 'progress': 0.6630525183185255, + 'bytes': 20447232, + 'rate': None, + 'estimated': None, + 'event': \{'isTrusted': True}, + 'upload': True +} +``` diff --git a/docs/library/graphing/charts/areachart.md b/docs/library/graphing/charts/areachart.md new file mode 100644 index 00000000000..c857dc3a704 --- /dev/null +++ b/docs/library/graphing/charts/areachart.md @@ -0,0 +1,444 @@ +--- +components: + - rx.recharts.AreaChart + - rx.recharts.Area +--- + +# Area Chart + +```python exec +import reflex as rx +import random +``` + +A Recharts area chart displays quantitative data using filled areas between a line connecting data points and the axis. + +## Basic Example + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def area_simple(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + width = "100%", + height = 250, + ) +``` + +## Syncing Charts + +The `sync_id` prop allows you to sync two graphs. In the example, it is set to "1" for both charts, indicating that they should be synchronized. This means that any interactions (such as brushing) performed on one chart will be reflected in the other chart. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def area_sync(): + return rx.vstack( + rx.recharts.bar_chart( + rx.recharts.graphing_tooltip(), + rx.recharts.bar( + data_key="uv", stroke="#8884d8", fill="#8884d8" + ), + rx.recharts.bar( + data_key="pv", stroke="#82ca9d", fill="#82ca9d", + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + sync_id="1", + width = "100%", + height = 200, + ), + rx.recharts.composed_chart( + rx.recharts.area( + data_key="uv", stroke="#8884d8", fill="#8884d8" + ), + rx.recharts.line( + data_key="pv", type_="monotone", stroke="#ff7300", + ), + + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.graphing_tooltip(), + rx.recharts.brush( + data_key="name", height=30, stroke="#8884d8" + ), + data=data, + sync_id="1", + width = "100%", + height = 250, + ), + width="100%", + ) +``` + +## Stacking Charts + +The `stack_id` prop allows you to stack multiple graphs on top of each other. In the example, it is set to "1" for both charts, indicating that they should be stacked together. This means that the bars or areas of the charts will be vertically stacked, with the values of each chart contributing to the total height of the stacked areas or bars. + +This is similar to the `sync_id` prop, but instead of synchronizing the interaction between the charts, it just stacks the charts on top of each other. + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def area_stack(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + stack_id="1", + ), + rx.recharts.area( + data_key="pv", + stroke=rx.color("green", 9), + fill=rx.color("green", 8), + stack_id="1", + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + stack_offset="none", + margin={"top": 5, "right": 5, "bottom": 5, "left": 5}, + width = "100%", + height = 300, + ) +``` + +## Multiple Axis + +Multiple axes can be used for displaying different data series with varying scales or units on the same chart. This allows for a more comprehensive comparison and analysis of the data. + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def area_multi_axis(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", stroke="#8884d8", fill="#8884d8", x_axis_id="primary", y_axis_id="left", + ), + rx.recharts.area( + data_key="pv", x_axis_id="secondary", y_axis_id="right", type_="monotone", stroke="#82ca9d", fill="#82ca9d" + ), + rx.recharts.x_axis(data_key="name", x_axis_id="primary"), + rx.recharts.x_axis(data_key="name", x_axis_id="secondary", orientation="top"), + rx.recharts.y_axis(data_key="uv", y_axis_id="left"), + rx.recharts.y_axis(data_key="pv", y_axis_id="right", orientation="right"), + rx.recharts.graphing_tooltip(), + rx.recharts.legend(), + data=data, + width = "100%", + height = 300, + ) +``` + +## Layout + +Use the `layout` prop to set the orientation to either `"horizontal"` (default) or `"vertical"`. + +```md alert info +# Include margins around your graph to ensure proper spacing and enhance readability. By default, provide margins on all sides of the chart to create a visually appealing and functional representation of your data. +``` + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def area_vertical(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + stroke=rx.color("accent", 8), + fill=rx.color("accent", 3), + ), + rx.recharts.x_axis(type_="number"), + rx.recharts.y_axis(data_key="name", type_="category"), + data=data, + layout="vertical", + height = 300, + width = "100%", + ) +``` + +## Stateful Example + +Here is an example of an area graph with a `State`. Here we have defined a function `randomize_data`, which randomly changes the data for both graphs when the first defined `area` is clicked on using `on_click=AreaState.randomize_data`. + +```python demo exec +class AreaState(rx.State): + data = data + curve_type = "" + + @rx.event + def randomize_data(self): + for i in range(len(self.data)): + self.data[i]["uv"] = random.randint(0, 10000) + self.data[i]["pv"] = random.randint(0, 10000) + self.data[i]["amt"] = random.randint(0, 10000) + + def change_curve_type(self, type_input): + self.curve_type = type_input + +def area_stateful(): + return rx.vstack( + rx.hstack( + rx.text("Curve Type:"), + rx.select( + [ + 'basis', + 'natural', + 'step' + ], + on_change = AreaState.change_curve_type, + default_value = 'basis', + ), + ), + rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + on_click=AreaState.randomize_data, + type_ = AreaState.curve_type, + ), + rx.recharts.area( + data_key="pv", + stroke="#82ca9d", + fill="#82ca9d", + on_click=AreaState.randomize_data, + type_ = AreaState.curve_type, + ), + rx.recharts.x_axis( + data_key="name", + ), + rx.recharts.y_axis(), + rx.recharts.legend(), + rx.recharts.cartesian_grid(), + data=AreaState.data, + width = "100%", + height=400, + ), + width="100%", + ) +``` diff --git a/docs/library/graphing/charts/barchart.md b/docs/library/graphing/charts/barchart.md new file mode 100644 index 00000000000..dd5de4efa3f --- /dev/null +++ b/docs/library/graphing/charts/barchart.md @@ -0,0 +1,388 @@ +--- +components: + - rx.recharts.BarChart + - rx.recharts.Bar +--- + +# Bar Chart + +```python exec +import reflex as rx +import random +from pcweb.pages.docs import library +``` + +A bar chart presents categorical data with rectangular bars with heights or lengths proportional to the values that they represent. + +For a bar chart we must define an `rx.recharts.bar()` component for each set of values we wish to plot. Each `rx.recharts.bar()` component has a `data_key` which clearly states which variable in our data we are tracking. In this simple example we plot `uv` as a bar against the `name` column which we set as the `data_key` in `rx.recharts.x_axis`. + +## Simple Example + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def bar_simple(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="uv", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + width = "100%", + height = 250, + ) +``` + +## Multiple Bars + +Multiple bars can be placed on the same `bar_chart`, using multiple `rx.recharts.bar()` components. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def bar_double(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="uv", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.bar( + data_key="pv", + stroke=rx.color("green", 9), + fill=rx.color("green", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + width = "100%", + height = 250, + ) +``` + +## Ranged Charts + +You can also assign a range in the bar by assigning the data_key in the `rx.recharts.bar` to a list with two elements, i.e. here a range of two temperatures for each date. + +```python demo graphing +range_data = [ + { + "day": "05-01", + "temperature": [ + -1, + 10 + ] + }, + { + "day": "05-02", + "temperature": [ + 2, + 15 + ] + }, + { + "day": "05-03", + "temperature": [ + 3, + 12 + ] + }, + { + "day": "05-04", + "temperature": [ + 4, + 12 + ] + }, + { + "day": "05-05", + "temperature": [ + 12, + 16 + ] + }, + { + "day": "05-06", + "temperature": [ + 5, + 16 + ] + }, + { + "day": "05-07", + "temperature": [ + 3, + 12 + ] + }, + { + "day": "05-08", + "temperature": [ + 0, + 8 + ] + }, + { + "day": "05-09", + "temperature": [ + -3, + 5 + ] + } +] + +def bar_range(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="temperature", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="day"), + rx.recharts.y_axis(), + data=range_data, + width = "100%", + height = 250, + ) +``` + +## Stateful Charts + +Here is an example of a bar graph with a `State`. Here we have defined a function `randomize_data`, which randomly changes the data for both graphs when the first defined `bar` is clicked on using `on_click=BarState.randomize_data`. + +```python demo exec +class BarState(rx.State): + data = data + + @rx.event + def randomize_data(self): + for i in range(len(self.data)): + self.data[i]["uv"] = random.randint(0, 10000) + self.data[i]["pv"] = random.randint(0, 10000) + self.data[i]["amt"] = random.randint(0, 10000) + +def bar_with_state(): + return rx.recharts.bar_chart( + rx.recharts.cartesian_grid( + stroke_dasharray="3 3", + ), + rx.recharts.bar( + data_key="uv", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.bar( + data_key="pv", + stroke=rx.color("green", 9), + fill=rx.color("green", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.legend(), + on_click=BarState.randomize_data, + data=BarState.data, + width = "100%", + height = 300, + ) +``` + +## Example with Props + +Here's an example demonstrates how to customize the appearance and layout of bars using the `bar_category_gap`, `bar_gap`, `bar_size`, and `max_bar_size` props. These props accept values in pixels to control the spacing and size of the bars. + +```python demo graphing + +data = [ + {'name': 'Page A', 'value': 2400}, + {'name': 'Page B', 'value': 1398}, + {'name': 'Page C', 'value': 9800}, + {'name': 'Page D', 'value': 3908}, + {'name': 'Page E', 'value': 4800}, + {'name': 'Page F', 'value': 3800}, +] + +def bar_features(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="value", + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + bar_category_gap="15%", + bar_gap=6, + bar_size=100, + max_bar_size=40, + width="100%", + height=300, + ) +``` + +## Vertical Example + +The `layout` prop allows you to set the orientation of the graph to be vertical or horizontal, it is set horizontally by default. + +```md alert info +# Include margins around your graph to ensure proper spacing and enhance readability. By default, provide margins on all sides of the chart to create a visually appealing and functional representation of your data. +``` + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def bar_vertical(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="uv", + stroke=rx.color("accent", 8), + fill=rx.color("accent", 3), + ), + rx.recharts.x_axis(type_="number"), + rx.recharts.y_axis(data_key="name", type_="category"), + data=data, + layout="vertical", + margin={ + "top": 20, + "right": 20, + "left": 20, + "bottom": 20 + }, + width = "100%", + height = 300, + + ) +``` + +To learn how to use the `sync_id`, `stack_id`,`x_axis_id` and `y_axis_id` props check out the of the area chart [documentation]({library.graphing.charts.areachart.path}), where these props are all described with examples. diff --git a/docs/library/graphing/charts/composedchart.md b/docs/library/graphing/charts/composedchart.md new file mode 100644 index 00000000000..a2475f2becc --- /dev/null +++ b/docs/library/graphing/charts/composedchart.md @@ -0,0 +1,90 @@ +--- +components: + - rx.recharts.ComposedChart +--- + +```python exec +import reflex as rx +from pcweb.pages.docs import library +``` + +# Composed Chart + +A `composed_chart` is a higher-level component chart that is composed of multiple charts, where other charts are the children of the `composed_chart`. The charts are placed on top of each other in the order they are provided in the `composed_chart` function. + +```md alert info +# To learn more about individual charts, checkout: **[area_chart]({library.graphing.charts.areachart.path})**, **[line_chart]({library.graphing.charts.linechart.path})**, or **[bar_chart]({library.graphing.charts.barchart.path})**. +``` + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def composed(): + return rx.recharts.composed_chart( + rx.recharts.area( + data_key="uv", + stroke="#8884d8", + fill="#8884d8" + ), + rx.recharts.bar( + data_key="amt", + bar_size=20, + fill="#413ea0" + ), + rx.recharts.line( + data_key="pv", + type_="monotone", + stroke="#ff7300" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.cartesian_grid(stroke_dasharray="3 3"), + rx.recharts.graphing_tooltip(), + data=data, + height=250, + width="100%", + ) +``` diff --git a/docs/library/graphing/charts/errorbar.md b/docs/library/graphing/charts/errorbar.md new file mode 100644 index 00000000000..f488a2c7bab --- /dev/null +++ b/docs/library/graphing/charts/errorbar.md @@ -0,0 +1,101 @@ +--- +components: + - rx.recharts.ErrorBar +--- + +```python exec +import reflex as rx +``` + +# Error Bar + +An error bar is a graphical representation of the uncertainty or variability of a data point in a chart, depicted as a line extending from the data point parallel to one of the axes. The `data_key`, `width`, `stroke_width`, `stroke`, and `direction` props can be used to customize the appearance and behavior of the error bars, specifying the data source, dimensions, color, and orientation of the error bars. + +```python demo graphing +data = [ + { + "x": 45, + "y": 100, + "z": 150, + "errorY": [ + 30, + 20 + ], + "errorX": 5 + }, + { + "x": 100, + "y": 200, + "z": 200, + "errorY": [ + 20, + 30 + ], + "errorX": 3 + }, + { + "x": 120, + "y": 100, + "z": 260, + "errorY": 20, + "errorX": [ + 5, + 3 + ] + }, + { + "x": 170, + "y": 300, + "z": 400, + "errorY": [ + 15, + 18 + ], + "errorX": 4 + }, + { + "x": 140, + "y": 250, + "z": 280, + "errorY": 23, + "errorX": [ + 6, + 7 + ] + }, + { + "x": 150, + "y": 400, + "z": 500, + "errorY": [ + 21, + 10 + ], + "errorX": 4 + }, + { + "x": 110, + "y": 280, + "z": 200, + "errorY": 21, + "errorX": [ + 5, + 6 + ] + } +] + +def error(): + return rx.recharts.scatter_chart( + rx.recharts.scatter( + rx.recharts.error_bar(data_key="errorY", direction="y", width=4, stroke_width=2, stroke="red"), + rx.recharts.error_bar(data_key="errorX", direction="x", width=4, stroke_width=2), + data=data, + fill="#8884d8", + name="A"), + rx.recharts.x_axis(data_key="x", name="x", type_="number"), + rx.recharts.y_axis(data_key="y", name="y", type_="number"), + width = "100%", + height = 300, + ) +``` diff --git a/docs/library/graphing/charts/funnelchart.md b/docs/library/graphing/charts/funnelchart.md new file mode 100644 index 00000000000..69e98f84d26 --- /dev/null +++ b/docs/library/graphing/charts/funnelchart.md @@ -0,0 +1,210 @@ +--- +components: + - rx.recharts.FunnelChart + - rx.recharts.Funnel +--- + +```python exec +import reflex as rx +import random +rx.toast.provider() +``` + +# Funnel Chart + +A funnel chart is a graphical representation used to visualize how data moves through a process. In a funnel chart, the dependent variable’s value diminishes in the subsequent stages of the process. It can be used to demonstrate the flow of users through a business or sales process. + +## Simple Example + +```python demo graphing +data = [ + { + "value": 100, + "name": "Sent", + "fill": "#8884d8" + }, + { + "value": 80, + "name": "Viewed", + "fill": "#83a6ed" + }, + { + "value": 50, + "name": "Clicked", + "fill": "#8dd1e1" + }, + { + "value": 40, + "name": "Add to Cart", + "fill": "#82ca9d" + }, + { + "value": 26, + "name": "Purchased", + "fill": "#a4de6c" + } +] + +def funnel_simple(): + return rx.recharts.funnel_chart( + rx.recharts.funnel( + rx.recharts.label_list( + position="right", + data_key="name", + fill="#000", + stroke="none", + ), + data_key="value", + data=data, + ), + width="100%", + height=250, + ) +``` + +## Event Triggers + +Funnel chart supports `on_click`, `on_mouse_enter`, `on_mouse_leave` and `on_mouse_move` event triggers, allows you to interact with the funnel chart and perform specific actions based on user interactions. + +```python demo graphing +data = [ + { + "value": 100, + "name": "Sent", + "fill": "#8884d8" + }, + { + "value": 80, + "name": "Viewed", + "fill": "#83a6ed" + }, + { + "value": 50, + "name": "Clicked", + "fill": "#8dd1e1" + }, + { + "value": 40, + "name": "Add to Cart", + "fill": "#82ca9d" + }, + { + "value": 26, + "name": "Purchased", + "fill": "#a4de6c" + } +] + +def funnel_events(): + return rx.recharts.funnel_chart( + rx.recharts.funnel( + rx.recharts.label_list( + position="right", + data_key="name", + fill="#000", + stroke="none", + ), + data_key="value", + data=data, + ), + on_click=rx.toast("Clicked on funnel chart"), + on_mouse_enter=rx.toast("Mouse entered"), + on_mouse_leave=rx.toast("Mouse left"), + width="100%", + height=250, + ) +``` + +## Dynamic Data + +Here is an example of a funnel chart with a `State`. Here we have defined a function `randomize_data`, which randomly changes the data when the graph is clicked on using `on_click=FunnelState.randomize_data`. + +```python exec +data = [ + { + "value": 100, + "name": "Sent", + "fill": "#8884d8" + }, + { + "value": 80, + "name": "Viewed", + "fill": "#83a6ed" + }, + { + "value": 50, + "name": "Clicked", + "fill": "#8dd1e1" + }, + { + "value": 40, + "name": "Add to Cart", + "fill": "#82ca9d" + }, + { + "value": 26, + "name": "Purchased", + "fill": "#a4de6c" + } +] +``` + +```python demo exec +class FunnelState(rx.State): + data = data + + @rx.event + def randomize_data(self): + self.data[0]["value"] = 100 + for i in range(len(self.data) - 1): + self.data[i + 1]["value"] = self.data[i][ + "value" + ] - random.randint(0, 20) + + +def funnel_state(): + return rx.recharts.funnel_chart( + rx.recharts.funnel( + rx.recharts.label_list( + position="right", + data_key="name", + fill="#000", + stroke="none", + ), + data_key="value", + data=FunnelState.data, + on_click=FunnelState.randomize_data, + ), + rx.recharts.graphing_tooltip(), + width="100%", + height=250, + ) +``` + +## Changing the Chart Animation + +The `is_animation_active` prop can be used to turn off the animation, but defaults to `True`. `animation_begin` sets the delay before animation starts, `animation_duration` determines how long the animation lasts, and `animation_easing` defines the speed curve of the animation for smooth transitions. + +```python demo graphing +data = [ + {"name": "Visits", "value": 5000, "fill": "#8884d8"}, + {"name": "Cart", "value": 3000, "fill": "#83a6ed"}, + {"name": "Checkout", "value": 2500, "fill": "#8dd1e1"}, + {"name": "Purchase", "value": 1000, "fill": "#82ca9d"}, + ] + +def funnel_animation(): + return rx.recharts.funnel_chart( + rx.recharts.funnel( + data_key="value", + data=data, + animation_begin=300, + animation_duration=9000, + animation_easing="ease-in-out", + ), + rx.recharts.graphing_tooltip(), + rx.recharts.legend(), + width="100%", + height=300, + ) +``` diff --git a/docs/library/graphing/charts/linechart.md b/docs/library/graphing/charts/linechart.md new file mode 100644 index 00000000000..4df06cd466d --- /dev/null +++ b/docs/library/graphing/charts/linechart.md @@ -0,0 +1,309 @@ +--- +components: + - rx.recharts.LineChart + - rx.recharts.Line +--- + +# Line Chart + +```python exec +import random +from typing import Any +from pcweb.pages.docs import library +import reflex as rx +``` + +A line chart is a type of chart used to show information that changes over time. Line charts are created by plotting a series of several points and connecting them with a straight line. + +## Simple Example + +For a line chart we must define an `rx.recharts.line()` component for each set of values we wish to plot. Each `rx.recharts.line()` component has a `data_key` which clearly states which variable in our data we are tracking. In this simple example we plot `pv` and `uv` as separate lines against the `name` column which we set as the `data_key` in `rx.recharts.x_axis`. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def line_simple(): + return rx.recharts.line_chart( + rx.recharts.line( + data_key="pv", + ), + rx.recharts.line( + data_key="uv", + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + width = "100%", + height = 300, + ) +``` + +## Example with Props + +Our second example uses exactly the same data as our first example, except now we add some extra features to our line graphs. We add a `type_` prop to `rx.recharts.line` to style the lines differently. In addition, we add an `rx.recharts.cartesian_grid` to get a grid in the background, an `rx.recharts.legend` to give us a legend for our graphs and an `rx.recharts.graphing_tooltip` to add a box with text that appears when you pause the mouse pointer on an element in the graph. + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def line_features(): + return rx.recharts.line_chart( + rx.recharts.line( + data_key="pv", + type_="monotone", + stroke="#8884d8",), + rx.recharts.line( + data_key="uv", + type_="monotone", + stroke="#82ca9d",), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.cartesian_grid(stroke_dasharray="3 3"), + rx.recharts.graphing_tooltip(), + rx.recharts.legend(), + data=data, + width = "100%", + height = 300, + ) +``` + +## Layout + +The `layout` prop allows you to set the orientation of the graph to be vertical or horizontal. The `margin` prop defines the spacing around the graph, + +```md alert info +# Include margins around your graph to ensure proper spacing and enhance readability. By default, provide margins on all sides of the chart to create a visually appealing and functional representation of your data. +``` + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def line_vertical(): + return rx.recharts.line_chart( + rx.recharts.line( + data_key="pv", + stroke=rx.color("accent", 9), + ), + rx.recharts.line( + data_key="uv", + stroke=rx.color("green", 9), + ), + rx.recharts.x_axis(type_="number"), + rx.recharts.y_axis(data_key="name", type_="category"), + layout="vertical", + margin={ + "top": 20, + "right": 20, + "left": 20, + "bottom": 20 + }, + data = data, + height = 300, + width = "100%", + ) +``` + +## Dynamic Data + +Chart data can be modified by tying the `data` prop to a State var. Most other +props, such as `type_`, can be controlled dynamically as well. In the following +example the "Munge Data" button can be used to randomly modify the data, and the +two `select` elements change the line `type_`. Since the data and style is saved +in the per-browser-tab State, the changes will not be visible to other visitors. + +```python demo exec + +initial_data = data + +class LineChartState(rx.State): + data: list[dict[str, Any]] = initial_data + pv_type: str = "monotone" + uv_type: str = "monotone" + + @rx.event + def set_pv_type(self, pv_type: str): + self.pv_type = pv_type + + @rx.event + def set_uv_type(self, uv_type: str): + self.uv_type = uv_type + + @rx.event + def munge_data(self): + for row in self.data: + row["uv"] += random.randint(-500, 500) + row["pv"] += random.randint(-1000, 1000) + +def line_dynamic(): + return rx.vstack( + rx.recharts.line_chart( + rx.recharts.line( + data_key="pv", + type_=LineChartState.pv_type, + stroke="#8884d8", + ), + rx.recharts.line( + data_key="uv", + type_=LineChartState.uv_type, + stroke="#82ca9d", + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=LineChartState.data, + margin={ + "top": 20, + "right": 20, + "left": 20, + "bottom": 20 + }, + width = "100%", + height = 300, + ), + rx.hstack( + rx.button("Munge Data", on_click=LineChartState.munge_data), + rx.select( + ["monotone", "linear", "step", "stepBefore", "stepAfter"], + value=LineChartState.pv_type, + on_change=LineChartState.set_pv_type + ), + rx.select( + ["monotone", "linear", "step", "stepBefore", "stepAfter"], + value=LineChartState.uv_type, + on_change=LineChartState.set_uv_type + ), + ), + width="100%", + ) +``` + +To learn how to use the `sync_id`, `x_axis_id` and `y_axis_id` props check out the of the area chart [documentation]({library.graphing.charts.areachart.path}), where these props are all described with examples. diff --git a/docs/library/graphing/charts/piechart.md b/docs/library/graphing/charts/piechart.md new file mode 100644 index 00000000000..999780ad72f --- /dev/null +++ b/docs/library/graphing/charts/piechart.md @@ -0,0 +1,216 @@ +--- +components: + - rx.recharts.PieChart + - rx.recharts.Pie +--- + +# Pie Chart + +```python exec +import reflex as rx +``` + +A pie chart is a circular statistical graphic which is divided into slices to illustrate numerical proportion. + +For a pie chart we must define an `rx.recharts.pie()` component for each set of values we wish to plot. Each `rx.recharts.pie()` component has a `data`, a `data_key` and a `name_key` which clearly states which data and which variables in our data we are tracking. In this simple example we plot `value` column as our `data_key` against the `name` column which we set as our `name_key`. +We also use the `fill` prop to set the color of the pie slices. + +```python demo graphing + +data01 = [ + { + "name": "Group A", + "value": 400 + }, + { + "name": "Group B", + "value": 300, + "fill":"#AC0E08FF" + }, + { + "name": "Group C", + "value": 300, + "fill":"rgb(80,40, 190)" + }, + { + "name": "Group D", + "value": 200, + "fill":rx.color("yellow", 10) + }, + { + "name": "Group E", + "value": 278, + "fill":"purple" + }, + { + "name": "Group F", + "value": 189, + "fill":"orange" + } +] + +def pie_simple(): + return rx.recharts.pie_chart( + rx.recharts.pie( + data=data01, + data_key="value", + name_key="name", + fill="#8884d8", + label=True, + ), + width="100%", + height=300, + ) +``` + +We can also add two pies on one chart by using two `rx.recharts.pie` components. + +In this example `inner_radius` and `outer_radius` props are used. They define the doughnut shape of a pie chart: `inner_radius` creates the hollow center (use "0%" for a full pie), while `outer_radius` sets the overall size. The `padding_angle` prop, used on the green pie below, adds space between pie slices, enhancing visibility of individual segments. + +```python demo graphing + +data01 = [ + { + "name": "Group A", + "value": 400 + }, + { + "name": "Group B", + "value": 300 + }, + { + "name": "Group C", + "value": 300 + }, + { + "name": "Group D", + "value": 200 + }, + { + "name": "Group E", + "value": 278 + }, + { + "name": "Group F", + "value": 189 + } +] +data02 = [ + { + "name": "Group A", + "value": 2400 + }, + { + "name": "Group B", + "value": 4567 + }, + { + "name": "Group C", + "value": 1398 + }, + { + "name": "Group D", + "value": 9800 + }, + { + "name": "Group E", + "value": 3908 + }, + { + "name": "Group F", + "value": 4800 + } +] + + +def pie_double(): + return rx.recharts.pie_chart( + rx.recharts.pie( + data=data01, + data_key="value", + name_key="name", + fill="#82ca9d", + inner_radius="60%", + padding_angle=5, + ), + rx.recharts.pie( + data=data02, + data_key="value", + name_key="name", + fill="#8884d8", + outer_radius="50%", + ), + rx.recharts.graphing_tooltip(), + width="100%", + height=300, + ) +``` + +## Dynamic Data + +Chart data tied to a State var causes the chart to automatically update when the +state changes, providing a nice way to visualize data in response to user +interface elements. View the "Data" tab to see the substate driving this +half-pie chart. + +```python demo exec +from typing import Any + + +class PieChartState(rx.State): + resources: list[dict[str, Any]] = [ + dict(type_="🏆", count=1), + dict(type_="🪵", count=1), + dict(type_="🥑", count=1), + dict(type_="🧱", count=1), + ] + + @rx.var(cache=True) + def resource_types(self) -> list[str]: + return [r["type_"] for r in self.resources] + + @rx.event + def increment(self, type_: str): + for resource in self.resources: + if resource["type_"] == type_: + resource["count"] += 1 + break + + @rx.event + def decrement(self, type_: str): + for resource in self.resources: + if resource["type_"] == type_ and resource["count"] > 0: + resource["count"] -= 1 + break + + +def dynamic_pie_example(): + return rx.hstack( + rx.recharts.pie_chart( + rx.recharts.pie( + data=PieChartState.resources, + data_key="count", + name_key="type_", + cx="50%", + cy="50%", + start_angle=180, + end_angle=0, + fill="#8884d8", + label=True, + ), + rx.recharts.graphing_tooltip(), + ), + rx.vstack( + rx.foreach( + PieChartState.resource_types, + lambda type_, i: rx.hstack( + rx.button("-", on_click=PieChartState.decrement(type_)), + rx.text(type_, PieChartState.resources[i]["count"]), + rx.button("+", on_click=PieChartState.increment(type_)), + ), + ), + ), + width="100%", + height="15em", + ) +``` diff --git a/docs/library/graphing/charts/radarchart.md b/docs/library/graphing/charts/radarchart.md new file mode 100644 index 00000000000..699ce1e2e33 --- /dev/null +++ b/docs/library/graphing/charts/radarchart.md @@ -0,0 +1,285 @@ +--- +components: + - rx.recharts.RadarChart + - rx.recharts.Radar +--- + +# Radar Chart + +```python exec +import reflex as rx +from typing import Any +``` + +A radar chart shows multivariate data of three or more quantitative variables mapped onto an axis. + +## Simple Example + +For a radar chart we must define an `rx.recharts.radar()` component for each set of values we wish to plot. Each `rx.recharts.radar()` component has a `data_key` which clearly states which variable in our data we are plotting. In this simple example we plot the `A` column of our data against the `subject` column which we set as the `data_key` in `rx.recharts.polar_angle_axis`. + +```python demo graphing +data = [ + { + "subject": "Math", + "A": 120, + "B": 110, + "fullMark": 150 + }, + { + "subject": "Chinese", + "A": 98, + "B": 130, + "fullMark": 150 + }, + { + "subject": "English", + "A": 86, + "B": 130, + "fullMark": 150 + }, + { + "subject": "Geography", + "A": 99, + "B": 100, + "fullMark": 150 + }, + { + "subject": "Physics", + "A": 85, + "B": 90, + "fullMark": 150 + }, + { + "subject": "History", + "A": 65, + "B": 85, + "fullMark": 150 + } +] + +def radar_simple(): + return rx.recharts.radar_chart( + rx.recharts.radar( + data_key="A", + stroke="#8884d8", + fill="#8884d8", + ), + rx.recharts.polar_grid(), + rx.recharts.polar_angle_axis(data_key="subject"), + rx.recharts.polar_radius_axis(angle=90, domain=[0, 150]), + data=data, + width="100%", + height=300, + ) +``` + +## Multiple Radars + +We can also add two radars on one chart by using two `rx.recharts.radar` components. + +In this plot an `inner_radius` and an `outer_radius` are set which determine the chart's size and shape. The `inner_radius` sets the distance from the center to the innermost part of the chart (creating a hollow center if greater than zero), while the `outer_radius` defines the chart's overall size by setting the distance from the center to the outermost edge of the radar plot. + +```python demo graphing + +data = [ + { + "subject": "Math", + "A": 120, + "B": 110, + "fullMark": 150 + }, + { + "subject": "Chinese", + "A": 98, + "B": 130, + "fullMark": 150 + }, + { + "subject": "English", + "A": 86, + "B": 130, + "fullMark": 150 + }, + { + "subject": "Geography", + "A": 99, + "B": 100, + "fullMark": 150 + }, + { + "subject": "Physics", + "A": 85, + "B": 90, + "fullMark": 150 + }, + { + "subject": "History", + "A": 65, + "B": 85, + "fullMark": 150 + } +] + +def radar_multiple(): + return rx.recharts.radar_chart( + rx.recharts.radar( + data_key="A", + stroke="#8884d8", + fill="#8884d8", + ), + rx.recharts.radar( + data_key="B", + stroke="#82ca9d", + fill="#82ca9d", + fill_opacity=0.6, + ), + rx.recharts.polar_grid(), + rx.recharts.polar_angle_axis(data_key="subject"), + rx.recharts.polar_radius_axis(angle=90, domain=[0, 150]), + rx.recharts.legend(), + data=data, + inner_radius="15%", + outer_radius="80%", + width="100%", + height=300, + ) + +``` + +## Using More Props + +The `dot` prop shows points at each data vertex when true. `legend_type="line"` displays a line in the chart legend. `animation_begin=0` starts the animation immediately, `animation_duration=8000` sets an 8-second animation, and `animation_easing="ease-in"` makes the animation start slowly and speed up. These props control the chart's appearance and animation behavior. + +```python demo graphing + +data = [ + { + "subject": "Math", + "A": 120, + "B": 110, + "fullMark": 150 + }, + { + "subject": "Chinese", + "A": 98, + "B": 130, + "fullMark": 150 + }, + { + "subject": "English", + "A": 86, + "B": 130, + "fullMark": 150 + }, + { + "subject": "Geography", + "A": 99, + "B": 100, + "fullMark": 150 + }, + { + "subject": "Physics", + "A": 85, + "B": 90, + "fullMark": 150 + }, + { + "subject": "History", + "A": 65, + "B": 85, + "fullMark": 150 + } + ] + + +def radar_start_end(): + return rx.recharts.radar_chart( + rx.recharts.radar( + data_key="A", + dot=True, + stroke="#8884d8", + fill="#8884d8", + fill_opacity=0.6, + legend_type="line", + animation_begin=0, + animation_duration=8000, + animation_easing="ease-in", + ), + rx.recharts.polar_grid(), + rx.recharts.polar_angle_axis(data_key="subject"), + rx.recharts.polar_radius_axis(angle=90, domain=[0, 150]), + rx.recharts.legend(), + data=data, + width="100%", + height=300, + ) + +``` + +# Dynamic Data + +Chart data tied to a State var causes the chart to automatically update when the +state changes, providing a nice way to visualize data in response to user +interface elements. View the "Data" tab to see the substate driving this +radar chart of character traits. + +```python demo exec +class RadarChartState(rx.State): + total_points: int = 100 + traits: list[dict[str, Any]] = [ + dict(trait="Strength", value=15), + dict(trait="Dexterity", value=15), + dict(trait="Constitution", value=15), + dict(trait="Intelligence", value=15), + dict(trait="Wisdom", value=15), + dict(trait="Charisma", value=15), + ] + + @rx.var + def remaining_points(self) -> int: + return self.total_points - sum(t["value"] for t in self.traits) + + @rx.var(cache=True) + def trait_names(self) -> list[str]: + return [t["trait"] for t in self.traits] + + @rx.event + def set_trait(self, trait: str, value: int): + for t in self.traits: + if t["trait"] == trait: + available_points = self.remaining_points + t["value"] + value = min(value, available_points) + t["value"] = value + break + +def radar_dynamic(): + return rx.hstack( + rx.recharts.radar_chart( + rx.recharts.radar( + data_key="value", + stroke="#8884d8", + fill="#8884d8", + ), + rx.recharts.polar_grid(), + rx.recharts.polar_angle_axis(data_key="trait"), + data=RadarChartState.traits, + ), + rx.vstack( + rx.foreach( + RadarChartState.trait_names, + lambda trait_name, i: rx.hstack( + rx.text(trait_name, width="7em"), + rx.slider( + default_value=RadarChartState.traits[i]["value"].to(int), + on_change=lambda value: RadarChartState.set_trait(trait_name, value[0]), + width="25vw", + ), + rx.text(RadarChartState.traits[i]['value']), + ), + ), + rx.text("Remaining points: ", RadarChartState.remaining_points), + ), + width="100%", + height="15em", + ) +``` diff --git a/docs/library/graphing/charts/radialbarchart.md b/docs/library/graphing/charts/radialbarchart.md new file mode 100644 index 00000000000..b9c6b93656e --- /dev/null +++ b/docs/library/graphing/charts/radialbarchart.md @@ -0,0 +1,110 @@ +--- +components: + - rx.recharts.RadialBarChart +--- + +# Radial Bar Chart + +```python exec +import reflex as rx +``` + +## Simple Example + +This example demonstrates how to use a `radial_bar_chart` with a `radial_bar`. The `radial_bar_chart` takes in `data` and then the `radial_bar` takes in a `data_key`. A radial bar chart is a circular visualization where data categories are represented by bars extending outward from a central point, with the length of each bar proportional to its value. + +```md alert info +# Fill color supports `rx.color()`, which automatically adapts to dark/light mode changes. +``` + +```python demo graphing +data = [ + {"name": "C", "x": 3, "fill": rx.color("cyan", 9)}, + {"name": "D", "x": 4, "fill": rx.color("blue", 9)}, + {"name": "E", "x": 5, "fill": rx.color("orange", 9)}, + {"name": "F", "x": 6, "fill": rx.color("red", 9)}, + {"name": "G", "x": 7, "fill": rx.color("gray", 9)}, + {"name": "H", "x": 8, "fill": rx.color("green", 9)}, + {"name": "I", "x": 9, "fill": rx.color("accent", 6)}, +] + +def radial_bar_simple(): + return rx.recharts.radial_bar_chart( + rx.recharts.radial_bar( + data_key="x", + min_angle=15, + ), + data=data, + width = "100%", + height = 500, + ) +``` + +## Advanced Example + +The `start_angle` and `end_angle` define the circular arc over which the bars are distributed, while `inner_radius` and `outer_radius` determine the radial extent of the bars from the center. + +```python demo graphing + +data_radial_bar = [ + { + "name": "18-24", + "uv": 31.47, + "pv": 2400, + "fill": "#8884d8" + }, + { + "name": "25-29", + "uv": 26.69, + "pv": 4567, + "fill": "#83a6ed" + }, + { + "name": "30-34", + "uv": -15.69, + "pv": 1398, + "fill": "#8dd1e1" + }, + { + "name": "35-39", + "uv": 8.22, + "pv": 9800, + "fill": "#82ca9d" + }, + { + "name": "40-49", + "uv": -8.63, + "pv": 3908, + "fill": "#a4de6c" + }, + { + "name": "50+", + "uv": -2.63, + "pv": 4800, + "fill": "#d0ed57" + }, + { + "name": "unknown", + "uv": 6.67, + "pv": 4800, + "fill": "#ffc658" + } +] + +def radial_bar_advanced(): + return rx.recharts.radial_bar_chart( + rx.recharts.radial_bar( + data_key="uv", + min_angle=90, + background=True, + label={"fill": '#666', "position": 'insideStart'}, + ), + data=data_radial_bar, + inner_radius="10%", + outer_radius="80%", + start_angle=180, + end_angle=0, + width="100%", + height=300, + ) +``` diff --git a/docs/library/graphing/charts/scatterchart.md b/docs/library/graphing/charts/scatterchart.md new file mode 100644 index 00000000000..03afdd264b8 --- /dev/null +++ b/docs/library/graphing/charts/scatterchart.md @@ -0,0 +1,296 @@ +--- +components: + - rx.recharts.ScatterChart + - rx.recharts.Scatter +--- + +# Scatter Chart + +```python exec +import reflex as rx +from pcweb.templates.docpage import docgraphing +from pcweb.pages.docs import library +``` + +A scatter chart always has two value axes to show one set of numerical data along a horizontal (value) axis and another set of numerical values along a vertical (value) axis. The chart displays points at the intersection of an x and y numerical value, combining these values into single data points. + +## Simple Example + +For a scatter chart we must define an `rx.recharts.scatter()` component for each set of values we wish to plot. Each `rx.recharts.scatter()` component has a `data` prop which clearly states which data source we plot. We also must define `rx.recharts.x_axis()` and `rx.recharts.y_axis()` so that the graph knows what data to plot on each axis. + +```python demo graphing +data01 = [ + { + "x": 100, + "y": 200, + "z": 200 + }, + { + "x": 120, + "y": 100, + "z": 260 + }, + { + "x": 170, + "y": 300, + "z": 400 + }, + { + "x": 170, + "y": 250, + "z": 280 + }, + { + "x": 150, + "y": 400, + "z": 500 + }, + { + "x": 110, + "y": 280, + "z": 200 + } +] + +def scatter_simple(): + return rx.recharts.scatter_chart( + rx.recharts.scatter( + data=data01, + fill="#8884d8",), + rx.recharts.x_axis(data_key="x", type_="number"), + rx.recharts.y_axis(data_key="y"), + width = "100%", + height = 300, + ) +``` + +## Multiple Scatters + +We can also add two scatters on one chart by using two `rx.recharts.scatter()` components, and we can define an `rx.recharts.z_axis()` which represents a third column of data and is represented by the size of the dots in the scatter plot. + +```python demo graphing +data01 = [ + { + "x": 100, + "y": 200, + "z": 200 + }, + { + "x": 120, + "y": 100, + "z": 260 + }, + { + "x": 170, + "y": 300, + "z": 400 + }, + { + "x": 170, + "y": 250, + "z": 280 + }, + { + "x": 150, + "y": 350, + "z": 500 + }, + { + "x": 110, + "y": 280, + "z": 200 + } +] + +data02 = [ + { + "x": 200, + "y": 260, + "z": 240 + }, + { + "x": 240, + "y": 290, + "z": 220 + }, + { + "x": 190, + "y": 290, + "z": 250 + }, + { + "x": 198, + "y": 250, + "z": 210 + }, + { + "x": 180, + "y": 280, + "z": 260 + }, + { + "x": 210, + "y": 220, + "z": 230 + } +] + +def scatter_double(): + return rx.recharts.scatter_chart( + rx.recharts.scatter( + data=data01, + fill="#8884d8", + name="A" + ), + rx.recharts.scatter( + data=data02, + fill="#82ca9d", + name="B" + ), + rx.recharts.cartesian_grid(stroke_dasharray="3 3"), + rx.recharts.x_axis(data_key="x", type_="number"), + rx.recharts.y_axis(data_key="y"), + rx.recharts.z_axis(data_key="z", range=[60, 400], name="score"), + rx.recharts.legend(), + rx.recharts.graphing_tooltip(), + width="100%", + height=300, + ) +``` + +To learn how to use the `x_axis_id` and `y_axis_id` props, check out the Multiple Axis section of the area chart [documentation]({library.graphing.charts.areachart.path}). + +## Dynamic Data + +Chart data tied to a State var causes the chart to automatically update when the +state changes, providing a nice way to visualize data in response to user +interface elements. View the "Data" tab to see the substate driving this +calculation of iterations in the Collatz Conjecture for a given starting number. +Enter a starting number in the box below the chart to recalculate. + +```python demo exec +class ScatterChartState(rx.State): + data: list[dict[str, int]] = [] + + @rx.event + def compute_collatz(self, form_data: dict) -> int: + n = int(form_data.get("start") or 1) + yield rx.set_value("start", "") + self.data = [] + for ix in range(400): + self.data.append({"x": ix, "y": n}) + if n == 1: + break + if n % 2 == 0: + n = n // 2 + else: + n = 3 * n + 1 + + +def scatter_dynamic(): + return rx.vstack( + rx.recharts.scatter_chart( + rx.recharts.scatter( + data=ScatterChartState.data, + fill="#8884d8", + ), + rx.recharts.x_axis(data_key="x", type_="number"), + rx.recharts.y_axis(data_key="y", type_="number"), + ), + rx.form.root( + rx.input(placeholder="Enter a number", id="start"), + rx.button("Compute", type="submit"), + on_submit=ScatterChartState.compute_collatz, + ), + width="100%", + height="15em", + on_mount=ScatterChartState.compute_collatz({"start": "15"}), + ) +``` + +## Legend Type and Shape + +```python demo exec +class ScatterChartState2(rx.State): + + legend_types: list[str] = ["square", "circle", "cross", "diamond", "star", "triangle", "wye"] + + legend_type: str = "circle" + + shapes: list[str] = ["square", "circle", "cross", "diamond", "star", "triangle", "wye"] + + shape: str = "circle" + + data01 = [ + { + "x": 100, + "y": 200, + "z": 200 + }, + { + "x": 120, + "y": 100, + "z": 260 + }, + { + "x": 170, + "y": 300, + "z": 400 + }, + { + "x": 170, + "y": 250, + "z": 280 + }, + { + "x": 150, + "y": 400, + "z": 500 + }, + { + "x": 110, + "y": 280, + "z": 200 + } + ] + + @rx.event + def set_shape(self, shape: str): + self.shape = shape + + @rx.event + def set_legend_type(self, legend_type: str): + self.legend_type = legend_type + +def scatter_shape(): + return rx.vstack( + rx.recharts.scatter_chart( + rx.recharts.scatter( + data=data01, + fill="#8884d8", + legend_type=ScatterChartState2.legend_type, + shape=ScatterChartState2.shape, + ), + rx.recharts.x_axis(data_key="x", type_="number"), + rx.recharts.y_axis(data_key="y"), + rx.recharts.legend(), + width = "100%", + height = 300, + ), + rx.hstack( + rx.text("Legend Type: "), + rx.select( + ScatterChartState2.legend_types, + value=ScatterChartState2.legend_type, + on_change=ScatterChartState2.set_legend_type, + ), + rx.text("Shape: "), + rx.select( + ScatterChartState2.shapes, + value=ScatterChartState2.shape, + on_change=ScatterChartState2.set_shape, + ), + ), + width="100%", + ) +``` diff --git a/docs/library/graphing/general/axis.md b/docs/library/graphing/general/axis.md new file mode 100644 index 00000000000..4c2d3d0b75f --- /dev/null +++ b/docs/library/graphing/general/axis.md @@ -0,0 +1,296 @@ +--- +components: + - rx.recharts.XAxis + - rx.recharts.YAxis + - rx.recharts.ZAxis +--- + +```python exec +import reflex as rx +``` + +# Axis + +The Axis component in Recharts is a powerful tool for customizing and configuring the axes of your charts. It provides a wide range of props that allow you to control the appearance, behavior, and formatting of the axis. Whether you're working with an AreaChart, LineChart, or any other chart type, the Axis component enables you to create precise and informative visualizations. + +## Basic Example + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def axis_simple(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis( + data_key="name", + label={"value": 'Pages', "position": "bottom"}, + ), + rx.recharts.y_axis( + data_key="uv", + label={"value": 'Views', "angle": -90, "position": "left"}, + ), + data=data, + width="100%", + height=300, + margin={ + "bottom": 40, + "left": 40, + "right": 40, + }, + ) +``` + +## Multiple Axes + +Multiple axes can be used for displaying different data series with varying scales or units on the same chart. This allows for a more comprehensive comparison and analysis of the data. + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def multi_axis(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", stroke="#8884d8", fill="#8884d8", y_axis_id="left", + ), + rx.recharts.area( + data_key="pv", y_axis_id="right", type_="monotone", stroke="#82ca9d", fill="#82ca9d" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(data_key="uv", y_axis_id="left"), + rx.recharts.y_axis(data_key="pv", y_axis_id="right", orientation="right"), + rx.recharts.graphing_tooltip(), + rx.recharts.legend(), + data=data, + width = "100%", + height = 300, + ) +``` + +## Choosing Location of Labels for Axes + +The axes `label` can take several positions. The example below allows you to try out different locations for the x and y axis labels. + +```python demo graphing + +class AxisState(rx.State): + + label_positions: list[str] = ["center", "insideTopLeft", "insideTopRight", "insideBottomRight", "insideBottomLeft", "insideTop", "insideBottom", "insideLeft", "insideRight", "outside", "top", "bottom", "left", "right"] + + label_offsets: list[str] = ["-30", "-20", "-10", "0", "10", "20", "30"] + + x_axis_postion: str = "bottom" + + x_axis_offset: int + + y_axis_postion: str = "left" + + y_axis_offset: int + + @rx.event + @rx.event + def set_y_axis_position(self, position: str): + self.y_axis_position = position + + @rx.event + def set_x_axis_position(self, position: str): + self.x_axis_position = position + + @rx.event + def set_x_axis_offset(self, offset: str): + self.x_axis_offset = int(offset) + + @rx.event + def set_y_axis_offset(self, offset: str): + self.y_axis_offset = int(offset) + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def axis_labels(): + return rx.vstack( + rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + stroke=rx.color("accent", 9), + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis( + data_key="name", + label={"value": 'Pages', "position": AxisState.x_axis_postion, "offset": AxisState.x_axis_offset}, + ), + rx.recharts.y_axis( + data_key="uv", + label={"value": 'Views', "angle": -90, "position": AxisState.y_axis_postion, "offset": AxisState.y_axis_offset}, + ), + data=data, + width="100%", + height=300, + margin={ + "bottom": 40, + "left": 40, + "right": 40, + } + ), + rx.hstack( + rx.text("X Label Position: "), + rx.select( + AxisState.label_positions, + value=AxisState.x_axis_postion, + on_change=AxisState.set_x_axis_postion, + ), + rx.text("X Label Offset: "), + rx.select( + AxisState.label_offsets, + value=AxisState.x_axis_offset.to_string(), + on_change=AxisState.set_x_axis_offset, + ), + rx.text("Y Label Position: "), + rx.select( + AxisState.label_positions, + value=AxisState.y_axis_postion, + on_change=AxisState.set_y_axis_postion, + ), + rx.text("Y Label Offset: "), + rx.select( + AxisState.label_offsets, + value=AxisState.y_axis_offset.to_string(), + on_change=AxisState.set_y_axis_offset, + ), + ), + width="100%", + ) +``` diff --git a/docs/library/graphing/general/brush.md b/docs/library/graphing/general/brush.md new file mode 100644 index 00000000000..9ac8ec6840a --- /dev/null +++ b/docs/library/graphing/general/brush.md @@ -0,0 +1,154 @@ +--- +components: + - rx.recharts.Brush +--- + +# Brush + +```python exec +import reflex as rx +``` + +## Simple Example + +The brush component allows us to view charts that have a large number of data points. To view and analyze them efficiently, the brush provides a slider with two handles that helps the viewer to select some range of data points to be displayed. + +```python demo graphing +data = [ + { "name": '1', "uv": 300, "pv": 456 }, + { "name": '2', "uv": -145, "pv": 230 }, + { "name": '3', "uv": -100, "pv": 345 }, + { "name": '4', "uv": -8, "pv": 450 }, + { "name": '5', "uv": 100, "pv": 321 }, + { "name": '6', "uv": 9, "pv": 235 }, + { "name": '7', "uv": 53, "pv": 267 }, + { "name": '8', "uv": 252, "pv": -378 }, + { "name": '9', "uv": 79, "pv": -210 }, + { "name": '10', "uv": 294, "pv": -23 }, + { "name": '12', "uv": 43, "pv": 45 }, + { "name": '13', "uv": -74, "pv": 90 }, + { "name": '14', "uv": -71, "pv": 130 }, + { "name": '15', "uv": -117, "pv": 11 }, + { "name": '16', "uv": -186, "pv": 107 }, + { "name": '17', "uv": -16, "pv": 926 }, + { "name": '18', "uv": -125, "pv": 653 }, + { "name": '19', "uv": 222, "pv": 366 }, + { "name": '20', "uv": 372, "pv": 486 }, + { "name": '21', "uv": 182, "pv": 512 }, + { "name": '22', "uv": 164, "pv": 302 }, + { "name": '23', "uv": 316, "pv": 425 }, + { "name": '24', "uv": 131, "pv": 467 }, + { "name": '25', "uv": 291, "pv": -190 }, + { "name": '26', "uv": -47, "pv": 194 }, + { "name": '27', "uv": -415, "pv": 371 }, + { "name": '28', "uv": -182, "pv": 376 }, + { "name": '29', "uv": -93, "pv": 295 }, + { "name": '30', "uv": -99, "pv": 322 }, + { "name": '31', "uv": -52, "pv": 246 }, + { "name": '32', "uv": 154, "pv": 33 }, + { "name": '33', "uv": 205, "pv": 354 }, + { "name": '34', "uv": 70, "pv": 258 }, + { "name": '35', "uv": -25, "pv": 359 }, + { "name": '36', "uv": -59, "pv": 192 }, + { "name": '37', "uv": -63, "pv": 464 }, + { "name": '38', "uv": -91, "pv": -2 }, + { "name": '39', "uv": -66, "pv": 154 }, + { "name": '40', "uv": -50, "pv": 186 }, +] + +def brush_simple(): + return rx.recharts.bar_chart( + rx.recharts.bar( + data_key="uv", + stroke="#8884d8", + fill="#8884d8" + ), + rx.recharts.bar( + data_key="pv", + stroke="#82ca9d", + fill="#82ca9d" + ), + rx.recharts.brush(data_key="name", height=30, stroke="#8884d8"), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + width="100%", + height=300, + ) +``` + +## Position, Size, and Range + +This example showcases ways to set the Position, Size, and Range. The `gap` prop provides the spacing between stops on the brush when the graph will refresh. The `start_index` and `end_index` props defines the default range of the brush. `traveller_width` prop specifies the width of each handle ("traveller" in recharts lingo). + +```python demo graphing +data = [ + { "name": '1', "uv": 300, "pv": 456 }, + { "name": '2', "uv": -145, "pv": 230 }, + { "name": '3', "uv": -100, "pv": 345 }, + { "name": '4', "uv": -8, "pv": 450 }, + { "name": '5', "uv": 100, "pv": 321 }, + { "name": '6', "uv": 9, "pv": 235 }, + { "name": '7', "uv": 53, "pv": 267 }, + { "name": '8', "uv": 252, "pv": -378 }, + { "name": '9', "uv": 79, "pv": -210 }, + { "name": '10', "uv": 294, "pv": -23 }, + { "name": '12', "uv": 43, "pv": 45 }, + { "name": '13', "uv": -74, "pv": 90 }, + { "name": '14', "uv": -71, "pv": 130 }, + { "name": '15', "uv": -117, "pv": 11 }, + { "name": '16', "uv": -186, "pv": 107 }, + { "name": '17', "uv": -16, "pv": 926 }, + { "name": '18', "uv": -125, "pv": 653 }, + { "name": '19', "uv": 222, "pv": 366 }, + { "name": '20', "uv": 372, "pv": 486 }, + { "name": '21', "uv": 182, "pv": 512 }, + { "name": '22', "uv": 164, "pv": 302 }, + { "name": '23', "uv": 316, "pv": 425 }, + { "name": '24', "uv": 131, "pv": 467 }, + { "name": '25', "uv": 291, "pv": -190 }, + { "name": '26', "uv": -47, "pv": 194 }, + { "name": '27', "uv": -415, "pv": 371 }, + { "name": '28', "uv": -182, "pv": 376 }, + { "name": '29', "uv": -93, "pv": 295 }, + { "name": '30', "uv": -99, "pv": 322 }, + { "name": '31', "uv": -52, "pv": 246 }, + { "name": '32', "uv": 154, "pv": 33 }, + { "name": '33', "uv": 205, "pv": 354 }, + { "name": '34', "uv": 70, "pv": 258 }, + { "name": '35', "uv": -25, "pv": 359 }, + { "name": '36', "uv": -59, "pv": 192 }, + { "name": '37', "uv": -63, "pv": 464 }, + { "name": '38', "uv": -91, "pv": -2 }, + { "name": '39', "uv": -66, "pv": 154 }, + { "name": '40', "uv": -50, "pv": 186 }, +] + +def brush_pos_size_range(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + stroke="#8884d8", + fill="#8884d8", + ), + rx.recharts.area( + data_key="pv", + stroke="#82ca9d", + fill="#82ca9d", + ), + rx.recharts.brush( + data_key="name", + traveller_width=15, + start_index=3, + end_index=10, + stroke=rx.color("mauve", 10), + fill=rx.color("mauve", 3), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data, + width="100%", + height=200, + ) + +``` diff --git a/docs/library/graphing/general/cartesiangrid.md b/docs/library/graphing/general/cartesiangrid.md new file mode 100644 index 00000000000..9e52a772f7a --- /dev/null +++ b/docs/library/graphing/general/cartesiangrid.md @@ -0,0 +1,204 @@ +--- +components: + - rx.recharts.CartesianGrid + # - rx.recharts.CartesianAxis +--- + +```python exec +import reflex as rx +``` + +# Cartesian Grid + +The Cartesian Grid is a component in Recharts that provides a visual reference for data points in charts. It helps users to better interpret the data by adding horizontal and vertical lines across the chart area. + +## Simple Example + +The `stroke_dasharray` prop in Recharts is used to create dashed or dotted lines for various chart elements like lines, axes, or grids. It's based on the SVG stroke-dasharray attribute. The `stroke_dasharray` prop accepts a comma-separated string of numbers that define a repeating pattern of dashes and gaps along the length of the stroke. + +- `stroke_dasharray="5,5"`: creates a line with 5-pixel dashes and 5-pixel gaps +- `stroke_dasharray="10,5,5,5"`: creates a more complex pattern with 10-pixel dashes, 5-pixel gaps, 5-pixel dashes, and 5-pixel gaps + +Here's a simple example using it on a Line component: + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def cgrid_simple(): + return rx.recharts.line_chart( + rx.recharts.line( + data_key="pv", + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.cartesian_grid(stroke_dasharray="4 4"), + data=data, + width = "100%", + height = 300, + ) +``` + +## Hidden Axes + +A `cartesian_grid` component can be used to hide the horizontal and vertical grid lines in a chart by setting the `horizontal` and `vertical` props to `False`. This can be useful when you want to show the grid lines only on one axis or when you want to create a cleaner look for the chart. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def cgrid_hidden(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="uv", + stroke="#8884d8", + fill="#8884d8" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.cartesian_grid( + stroke_dasharray="2 4", + vertical=False, + horizontal=True, + ), + data=data, + width = "100%", + height = 300, + ) +``` + +## Custom Grid Lines + +The `horizontal_points` and `vertical_points` props allow you to specify custom grid lines on the chart, offering fine-grained control over the grid's appearance. + +These props accept arrays of numbers, where each number represents a pixel offset: + +- For `horizontal_points`, the offset is measured from the top edge of the chart +- For `vertical_points`, the offset is measured from the left edge of the chart + +```md alert info +# **Important**: The values provided to these props are not directly related to the axis values. They represent pixel offsets within the chart's rendering area. +``` + +Here's an example demonstrating custom grid lines in a scatter chart: + +```python demo graphing + +data2 = [ + {"x": 100, "y": 200, "z": 200}, + {"x": 120, "y": 100, "z": 260}, + {"x": 170, "y": 300, "z": 400}, + {"x": 170, "y": 250, "z": 280}, + {"x": 150, "y": 400, "z": 500}, + {"x": 110, "y": 280, "z": 200}, + {"x": 200, "y": 150, "z": 300}, + {"x": 130, "y": 350, "z": 450}, + {"x": 90, "y": 220, "z": 180}, + {"x": 180, "y": 320, "z": 350}, + {"x": 140, "y": 230, "z": 320}, + {"x": 160, "y": 180, "z": 240}, +] + +def cgrid_custom(): + return rx.recharts.scatter_chart( + rx.recharts.scatter( + data=data2, + fill="#8884d8", + ), + rx.recharts.x_axis(data_key="x", type_="number"), + rx.recharts.y_axis(data_key="y"), + rx.recharts.cartesian_grid( + stroke_dasharray="3 3", + horizontal_points=[0, 25, 50], + vertical_points=[65, 90, 115], + ), + width = "100%", + height = 200, + ) +``` + +Use these props judiciously to enhance data visualization without cluttering the chart. They're particularly useful for highlighting specific data ranges or creating visual reference points. diff --git a/docs/library/graphing/general/label.md b/docs/library/graphing/general/label.md new file mode 100644 index 00000000000..0ea0caeb7ac --- /dev/null +++ b/docs/library/graphing/general/label.md @@ -0,0 +1,180 @@ +--- +components: + - rx.recharts.Label + - rx.recharts.LabelList +--- + +# Label + +```python exec +import reflex as rx +``` + +Label is a component used to display a single label at a specific position within a chart or axis, while LabelList is a component that automatically renders a list of labels for each data point in a chart series, providing a convenient way to display multiple labels without manually positioning each one. + +## Simple Example + +Here's a simple example that demonstrates how you can customize the label of your axis using `rx.recharts.label`. The `value` prop represents the actual text of the label, the `position` prop specifies where the label is positioned within the axis component, and the `offset` prop is used to fine-tune the label's position. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 5800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def label_simple(): + return rx.recharts.bar_chart( + rx.recharts.cartesian_grid( + stroke_dasharray="3 3" + ), + rx.recharts.bar( + rx.recharts.label_list( + data_key="uv", position="top" + ), + data_key="uv", + fill=rx.color("accent", 8), + ), + rx.recharts.x_axis( + rx.recharts.label( + value="center", + position="center", + offset=30, + ), + rx.recharts.label( + value="inside left", + position="insideLeft", + offset=10, + ), + rx.recharts.label( + value="inside right", + position="insideRight", + offset=10, + ), + height=50, + ), + data=data, + margin={ + "left": 20, + "right": 20, + "top": 20, + "bottom": 20, + }, + width="100%", + height=250, + ) +``` + +## Label List Example + +`rx.recharts.label_list` takes in a `data_key` where we define the data column to plot. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 5800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def label_list(): + return rx.recharts.bar_chart( + rx.recharts.bar( + rx.recharts.label_list(data_key="uv", position="top"), + data_key="uv", + stroke="#8884d8", + fill="#8884d8" + ), + rx.recharts.bar( + rx.recharts.label_list(data_key="pv", position="top"), + data_key="pv", + stroke="#82ca9d", + fill="#82ca9d" + ), + rx.recharts.x_axis( + data_key="name" + ), + rx.recharts.y_axis(), + margin={"left": 10, "right": 0, "top": 20, "bottom": 10}, + data=data, + width="100%", + height = 300, + ) +``` diff --git a/docs/library/graphing/general/legend.md b/docs/library/graphing/general/legend.md new file mode 100644 index 00000000000..0f60447905c --- /dev/null +++ b/docs/library/graphing/general/legend.md @@ -0,0 +1,171 @@ +--- +components: + - rx.recharts.Legend +--- + +# Legend + +```python exec +import reflex as rx +``` + +A legend tells what each plot represents. Just like on a map, the legend helps the reader understand what they are looking at. For a line graph for example it tells us what each line represents. + +## Simple Example + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def legend_simple(): + return rx.recharts.composed_chart( + rx.recharts.area( + data_key="uv", + stroke="#8884d8", + fill="#8884d8" + ), + rx.recharts.bar( + data_key="amt", + bar_size=20, + fill="#413ea0" + ), + rx.recharts.line( + data_key="pv", + type_="monotone", + stroke="#ff7300" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.legend(), + data=data, + width = "100%", + height = 300, + ) +``` + +## Example with Props + +The style and layout of the legend can be customized using a set of props. `width` and `height` set the dimensions of the container that wraps the legend, and `layout` can set the legend to display vertically or horizontally. `align` and `vertical_align` set the position relative to the chart container. The type and size of icons can be set using `icon_size` and `icon_type`. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def legend_props(): + return rx.recharts.composed_chart( + rx.recharts.line( + data_key="pv", + type_="monotone", + stroke=rx.color("accent", 7), + ), + rx.recharts.line( + data_key="amt", + type_="monotone", + stroke=rx.color("green", 7), + ), + rx.recharts.line( + data_key="uv", + type_="monotone", + stroke=rx.color("red", 7), + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.legend( + width=60, + height=100, + layout="vertical", + align="right", + vertical_align="top", + icon_size=15, + icon_type="square", + ), + data=data, + width="100%", + height=300, + ) + +``` diff --git a/docs/library/graphing/general/reference.md b/docs/library/graphing/general/reference.md new file mode 100644 index 00000000000..a470d282bc6 --- /dev/null +++ b/docs/library/graphing/general/reference.md @@ -0,0 +1,258 @@ +--- +components: + - rx.recharts.ReferenceLine + - rx.recharts.ReferenceDot + - rx.recharts.ReferenceArea +--- + +# Reference + +```python exec +import reflex as rx +``` + +The Reference components in Recharts, including ReferenceLine, ReferenceArea, and ReferenceDot, are used to add visual aids and annotations to the chart, helping to highlight specific data points, ranges, or thresholds for better data interpretation and analysis. + +## Reference Area + +The `rx.recharts.reference_area` component in Recharts is used to highlight a specific area or range on the chart by drawing a rectangular region. It is defined by specifying the coordinates (x1, x2, y1, y2) and can be used to emphasize important data ranges or intervals on the chart. + +```python demo graphing +data = [ + { + "x": 45, + "y": 100, + "z": 150, + "errorY": [ + 30, + 20 + ], + "errorX": 5 + }, + { + "x": 100, + "y": 200, + "z": 200, + "errorY": [ + 20, + 30 + ], + "errorX": 3 + }, + { + "x": 120, + "y": 100, + "z": 260, + "errorY": 20, + "errorX": [ + 10, + 3 + ] + }, + { + "x": 170, + "y": 300, + "z": 400, + "errorY": [ + 15, + 18 + ], + "errorX": 4 + }, + { + "x": 140, + "y": 250, + "z": 280, + "errorY": 23, + "errorX": [ + 6, + 7 + ] + }, + { + "x": 150, + "y": 400, + "z": 500, + "errorY": [ + 21, + 10 + ], + "errorX": 4 + }, + { + "x": 110, + "y": 280, + "z": 200, + "errorY": 21, + "errorX": [ + 1, + 8 + ] + } +] + +def reference(): + return rx.recharts.scatter_chart( + rx.recharts.scatter( + data=data, + fill="#8884d8", + name="A"), + rx.recharts.reference_area(x1= 150, x2=180, y1=150, y2=300, fill="#8884d8", fill_opacity=0.3), + rx.recharts.x_axis(data_key="x", name="x", type_="number"), + rx.recharts.y_axis(data_key="y", name="y", type_="number"), + rx.recharts.graphing_tooltip(), + width = "100%", + height = 300, + ) +``` + +## Reference Line + +The `rx.recharts.reference_line` component in rx.recharts is used to draw a horizontal or vertical line on the chart at a specified position. It helps to highlight important values, thresholds, or ranges on the axis, providing visual reference points for better data interpretation. + +```python demo graphing +data_2 = [ + {"name": "Page A", "uv": 4000, "pv": 2400, "amt": 2400}, + {"name": "Page B", "uv": 3000, "pv": 1398, "amt": 2210}, + {"name": "Page C", "uv": 2000, "pv": 9800, "amt": 2290}, + {"name": "Page D", "uv": 2780, "pv": 3908, "amt": 2000}, + {"name": "Page E", "uv": 1890, "pv": 4800, "amt": 2181}, + {"name": "Page F", "uv": 2390, "pv": 3800, "amt": 2500}, + {"name": "Page G", "uv": 3490, "pv": 4300, "amt": 2100}, +] + +def reference_line(): + return rx.recharts.area_chart( + rx.recharts.area( + data_key="pv", stroke=rx.color("accent", 8), fill=rx.color("accent", 7), + ), + rx.recharts.reference_line( + x = "Page C", + stroke = rx.color("accent", 10), + label="Max PV PAGE", + ), + rx.recharts.reference_line( + y = 9800, + stroke = rx.color("green", 10), + label="Max" + ), + rx.recharts.reference_line( + y = 4343, + stroke = rx.color("green", 10), + label="Average" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + data=data_2, + height = 300, + width = "100%", + ) + +``` + +## Reference Dot + +The `rx.recharts.reference_dot` component in Recharts is used to mark a specific data point on the chart with a customizable dot. It allows you to highlight important values, outliers, or thresholds by providing a visual reference marker at the specified coordinates (x, y) on the chart. + +```python demo graphing + +data_3 = [ + { + "x": 45, + "y": 100, + "z": 150, + }, + { + "x": 100, + "y": 200, + "z": 200, + }, + { + "x": 120, + "y": 100, + "z": 260, + }, + { + "x": 170, + "y": 300, + "z": 400, + }, + { + "x": 140, + "y": 250, + "z": 280, + }, + { + "x": 150, + "y": 400, + "z": 500, + }, + { + "x": 110, + "y": 280, + "z": 200, + }, + { + "x": 80, + "y": 150, + "z": 180, + }, + { + "x": 200, + "y": 350, + "z": 450, + }, + { + "x": 90, + "y": 220, + "z": 240, + }, + { + "x": 130, + "y": 320, + "z": 380, + }, + { + "x": 180, + "y": 120, + "z": 300, + }, +] + +def reference_dot(): + return rx.recharts.scatter_chart( + rx.recharts.scatter( + data=data_3, + fill=rx.color("accent", 9), + name="A", + ), + rx.recharts.x_axis( + data_key="x", name="x", type_="number" + ), + rx.recharts.y_axis( + data_key="y", name="y", type_="number" + ), + rx.recharts.reference_dot( + x = 160, + y = 350, + r = 15, + fill = rx.color("accent", 5), + stroke = rx.color("accent", 10), + ), + rx.recharts.reference_dot( + x = 170, + y = 300, + r = 20, + fill = rx.color("accent", 7), + ), + rx.recharts.reference_dot( + x = 90, + y = 220, + r = 18, + fill = rx.color("green", 7), + ), + height = 200, + width = "100%", + ) + +``` diff --git a/docs/library/graphing/general/tooltip.md b/docs/library/graphing/general/tooltip.md new file mode 100644 index 00000000000..c89037bd6f7 --- /dev/null +++ b/docs/library/graphing/general/tooltip.md @@ -0,0 +1,172 @@ +--- +components: + - rx.recharts.GraphingTooltip +--- + +# Tooltip + +```python exec +import reflex as rx +``` + +Tooltips are the little boxes that pop up when you hover over something. Tooltips are always attached to something, like a dot on a scatter chart, or a bar on a bar chart. + +```python demo graphing +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def tooltip_simple(): + return rx.recharts.composed_chart( + rx.recharts.area( + data_key="uv", + stroke="#8884d8", + fill="#8884d8" + ), + rx.recharts.bar( + data_key="amt", + bar_size=20, + fill="#413ea0" + ), + rx.recharts.line( + data_key="pv", + type_="monotone", + stroke="#ff7300" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.cartesian_grid(stroke_dasharray="3 3"), + rx.recharts.graphing_tooltip(), + data=data, + width = "100%", + height = 300, + ) +``` + +## Custom Styling + +The `rx.recharts.graphing_tooltip` component allows for customization of the tooltip's style, position, and layout. `separator` sets the separator between the data key and value. `view_box` prop defines the dimensions of the chart's viewbox while `allow_escape_view_box` determines whether the tooltip can extend beyond the viewBox horizontally (x) or vertically (y). `wrapper_style` prop allows you to style the outer container or wrapper of the tooltip. `content_style` prop allows you to style the inner content area of the tooltip. `is_animation_active` prop determines if the tooltip animation is active or not. + +```python demo graphing + +data = [ + { + "name": "Page A", + "uv": 4000, + "pv": 2400, + "amt": 2400 + }, + { + "name": "Page B", + "uv": 3000, + "pv": 1398, + "amt": 2210 + }, + { + "name": "Page C", + "uv": 2000, + "pv": 9800, + "amt": 2290 + }, + { + "name": "Page D", + "uv": 2780, + "pv": 3908, + "amt": 2000 + }, + { + "name": "Page E", + "uv": 1890, + "pv": 4800, + "amt": 2181 + }, + { + "name": "Page F", + "uv": 2390, + "pv": 3800, + "amt": 2500 + }, + { + "name": "Page G", + "uv": 3490, + "pv": 4300, + "amt": 2100 + } +] + +def tooltip_custom_styling(): + return rx.recharts.composed_chart( + rx.recharts.area( + data_key="uv", stroke="#8884d8", fill="#8884d8" + ), + rx.recharts.bar( + data_key="amt", bar_size=20, fill="#413ea0" + ), + rx.recharts.line( + data_key="pv", type_="monotone", stroke="#ff7300" + ), + rx.recharts.x_axis(data_key="name"), + rx.recharts.y_axis(), + rx.recharts.graphing_tooltip( + separator = " - ", + view_box = {"width" : 675, " height" : 300 }, + allow_escape_view_box={"x": True, "y": False}, + wrapper_style={ + "backgroundColor": rx.color("accent", 3), + "borderRadius": "8px", + "padding": "10px", + }, + content_style={ + "backgroundColor": rx.color("accent", 4), + "borderRadius": "4px", + "padding": "8px", + }, + position = {"x" : 600, "y" : 0}, + is_animation_active = False, + ), + data=data, + height = 300, + width = "100%", + ) +``` diff --git a/docs/library/graphing/other-charts/plotly.md b/docs/library/graphing/other-charts/plotly.md new file mode 100644 index 00000000000..3d036fac706 --- /dev/null +++ b/docs/library/graphing/other-charts/plotly.md @@ -0,0 +1,144 @@ +--- +components: + - rx.plotly +--- + +# Plotly + +```python exec +import reflex as rx +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go +``` + +Plotly is a graphing library that can be used to create interactive graphs. Use the rx.plotly component to wrap Plotly as a component for use in your web page. Checkout [Plotly](https://plotly.com/graphing-libraries/) for more information. + +```md alert info +# When integrating Plotly graphs into your UI code, note that the method for displaying the graph differs from a regular Python script. Instead of using `fig.show()`, use `rx.plotly(data=fig)` within your UI code to ensure the graph is properly rendered and displayed within the user interface +``` + +## Basic Example + +Let's create a line graph of life expectancy in Canada. + +```python demo exec +import plotly.express as px + +df = px.data.gapminder().query("country=='Canada'") +fig = px.line(df, x="year", y="lifeExp", title='Life expectancy in Canada') + +def line_chart(): + return rx.center( + rx.plotly(data=fig), + ) +``` + +## 3D graphing example + +Let's create a 3D surface plot of Mount Bruno. This is a slightly more complicated example, but it wraps in Reflex using the same method. In fact, you can wrap any figure using the same approach. + +```python demo exec +import plotly.graph_objects as go +import pandas as pd + +# Read data from a csv +z_data = pd.read_csv('data/mt_bruno_elevation.csv') + +fig = go.Figure(data=[go.Surface(z=z_data.values)]) +fig.update_traces(contours_z=dict(show=True, usecolormap=True, + highlightcolor="limegreen", project_z=True)) +fig.update_layout( + scene_camera_eye=dict(x=1.87, y=0.88, z=-0.64), + margin=dict(l=65, r=50, b=65, t=90) +) + +def mountain_surface(): + return rx.center( + rx.plotly(data=fig), + ) +``` + +📊 **Dataset source:** [mt_bruno_elevation.csv](https://raw.githubusercontent.com/plotly/datasets/master/api_docs/mt_bruno_elevation.csv) + +## Plot as State Var + +If the figure is set as a state var, it can be updated during run time. + +```python demo exec +import plotly.express as px +import plotly.graph_objects as go +import pandas as pd + +class PlotlyState(rx.State): + df: pd.DataFrame + figure: go.Figure = px.line() + + @rx.event + def create_figure(self): + self.df = px.data.gapminder().query("country=='Canada'") + self.figure = px.line( + self.df, + x="year", + y="lifeExp", + title="Life expectancy in Canada", + ) + + @rx.event + def set_selected_country(self, country): + self.df = px.data.gapminder().query(f"country=='{country}'") + self.figure = px.line( + self.df, + x="year", + y="lifeExp", + title=f"Life expectancy in {country}", + ) + + + +def line_chart_with_state(): + return rx.vstack( + rx.select( + ['China', 'France', 'United Kingdom', 'United States', 'Canada'], + default_value="Canada", + on_change=PlotlyState.set_selected_country, + ), + rx.plotly( + data=PlotlyState.figure, + on_mount=PlotlyState.create_figure, + ), + ) +``` + +## Adding Styles and Layouts + +Use `update_layout()` method to update the layout of your chart. Checkout [Plotly Layouts](https://plotly.com/python/reference/layout/) for all layouts props. + +```md alert info +Note that the width and height props are not recommended to ensure the plot remains size responsive to its container. The size of plot will be determined by it's outer container. +``` + +```python demo exec +df = px.data.gapminder().query("country=='Canada'") +fig_1 = px.line( + df, + x="year", + y="lifeExp", + title="Life expectancy in Canada", +) +fig_1.update_layout( + title_x=0.5, + plot_bgcolor="#c3d7f7", + paper_bgcolor="rgba(128, 128, 128, 0.1)", + showlegend=True, + title_font_family="Open Sans", + title_font_size=25, +) + +def add_styles(): + return rx.center( + rx.plotly(data=fig_1), + width="100%", + height="100%", + ) +``` diff --git a/docs/library/graphing/other-charts/pyplot.md b/docs/library/graphing/other-charts/pyplot.md new file mode 100644 index 00000000000..5e57071bf4d --- /dev/null +++ b/docs/library/graphing/other-charts/pyplot.md @@ -0,0 +1,155 @@ +--- +components: + - pyplot +--- + +```python exec +import reflex as rx +from reflex_pyplot import pyplot +import numpy as np +import random +import matplotlib.pyplot as plt +from matplotlib.figure import Figure +from reflex.style import toggle_color_mode +``` + +# Pyplot + +Pyplot (`reflex-pyplot`) is a graphing library that wraps Matplotlib. Use the `pyplot` component to display any Matplotlib plot in your app. Check out [Matplotlib](https://matplotlib.org/) for more information. + +## Installation + +Install the `reflex-pyplot` package using pip. + +```bash +pip install reflex-pyplot +``` + +## Basic Example + +To display a Matplotlib plot in your app, you can use the `pyplot` component. Pass in the figure you created with Matplotlib to the `pyplot` component as a child. + +```python demo exec +import matplotlib.pyplot as plt +import reflex as rx +from reflex_pyplot import pyplot +import numpy as np + +def create_contour_plot(): + X, Y = np.meshgrid(np.linspace(-3, 3, 256), np.linspace(-3, 3, 256)) + Z = (1 - X/2 + X**5 + Y**3) * np.exp(-X**2 - Y**2) + levels = np.linspace(Z.min(), Z.max(), 7) + + fig, ax = plt.subplots() + ax.contourf(X, Y, Z, levels=levels) + plt.close(fig) + return fig + +def pyplot_simple_example(): + return rx.card( + pyplot(create_contour_plot(), width="100%", height="400px"), + bg_color='#ffffff', + width="100%", + ) +``` + +```md alert info +# You must close the figure after creating + +Not closing the figure could cause memory issues. +``` + +## Stateful Example + +Lets create a scatter plot of random data. We'll also allow the user to randomize the data and change the number of points. + +In this example, we'll use a `color_mode_cond` to display the plot in both light and dark mode. We need to do this manually here because the colors are determined by the matplotlib chart and not the theme. + +```python demo exec +import random +from typing import Literal +import matplotlib.pyplot as plt +import reflex as rx +from reflex_pyplot import pyplot +import numpy as np + + +def create_plot(theme: str, plot_data: tuple, scale: list): + bg_color, text_color = ('#1e1e1e', 'white') if theme == 'dark' else ('white', 'black') + grid_color = '#555555' if theme == 'dark' else '#cccccc' + + fig, ax = plt.subplots(facecolor=bg_color) + ax.set_facecolor(bg_color) + + for (x, y), color in zip(plot_data, ["#4e79a7", "#f28e2b"]): + ax.scatter(x, y, c=color, s=scale, label=color, alpha=0.6, edgecolors="none") + + ax.legend(loc="upper right", facecolor=bg_color, edgecolor='none', labelcolor=text_color) + ax.grid(True, color=grid_color) + ax.tick_params(colors=text_color) + for spine in ax.spines.values(): + spine.set_edgecolor(text_color) + + for item in [ax.xaxis.label, ax.yaxis.label, ax.title]: + item.set_color(text_color) + plt.close(fig) + + return fig + +class PyplotState(rx.State): + num_points: int = 25 + plot_data: tuple = tuple(np.random.rand(2, 25) for _ in range(2)) + scale: list = [random.uniform(0, 100) for _ in range(25)] + + @rx.event(temporal=True, throttle=500) + def randomize(self): + self.plot_data = tuple(np.random.rand(2, self.num_points) for _ in range(2)) + self.scale = [random.uniform(0, 100) for _ in range(self.num_points)] + + @rx.event(temporal=True, throttle=500) + def set_num_points(self, num_points: list[int | float]): + self.num_points = int(num_points[0]) + yield PyplotState.randomize() + + @rx.var + def fig_light(self) -> Figure: + fig = create_plot("light", self.plot_data, self.scale) + return fig + + @rx.var + def fig_dark(self) -> Figure: + fig = create_plot("dark", self.plot_data, self.scale) + return fig + +def pyplot_example(): + return rx.vstack( + rx.card( + rx.color_mode_cond( + pyplot(PyplotState.fig_light, width="100%", height="100%"), + pyplot(PyplotState.fig_dark, width="100%", height="100%"), + ), + rx.vstack( + rx.hstack( + rx.button( + "Randomize", + on_click=PyplotState.randomize, + ), + rx.text("Number of Points:"), + rx.slider( + default_value=25, + min_=10, + max=100, + on_value_commit=PyplotState.set_num_points, + ), + width="100%", + ), + width="100%", + ), + width="100%", + ), + justify_content="center", + align_items="center", + height="100%", + width="100%", + ) +``` diff --git a/docs/library/layout/aspect_ratio.md b/docs/library/layout/aspect_ratio.md new file mode 100644 index 00000000000..cc5713af5aa --- /dev/null +++ b/docs/library/layout/aspect_ratio.md @@ -0,0 +1,85 @@ +--- +components: + - rx.aspect_ratio +--- + +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +``` + +# Aspect Ratio + +Displays content with a desired ratio. + +## Basic Example + +Setting the `ratio` prop will adjust the width or height +of the content such that the `width` divided by the `height` equals the `ratio`. +For responsive scaling, set the `width` or `height` of the content to `"100%"`. + +```python demo +rx.grid( + rx.aspect_ratio( + rx.box( + "Widescreen 16:9", + background_color="papayawhip", + width="100%", + height="100%", + ), + ratio=16 / 9, + ), + rx.aspect_ratio( + rx.box( + "Letterbox 4:3", + background_color="orange", + width="100%", + height="100%", + ), + ratio=4 / 3, + ), + rx.aspect_ratio( + rx.box( + "Square 1:1", + background_color="green", + width="100%", + height="100%", + ), + ratio=1, + ), + rx.aspect_ratio( + rx.box( + "Portrait 5:7", + background_color="lime", + width="100%", + height="100%", + ), + ratio=5 / 7, + ), + spacing="2", + width="25%", +) +``` + +```md alert warning +# Never set `height` or `width` directly on an `aspect_ratio` component or its contents. + +Instead, wrap the `aspect_ratio` in a `box` that constrains either the width or the height, then set the content width and height to `"100%"`. +``` + +```python demo +rx.flex( + *[ + rx.box( + rx.aspect_ratio( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="100%", height="100%"), + ratio=ratio, + ), + width="20%", + ) + for ratio in [16 / 9, 3 / 2, 2 / 3, 1] + ], + justify="between", + width="100%", +) +``` diff --git a/docs/library/layout/box.md b/docs/library/layout/box.md new file mode 100644 index 00000000000..188e791208a --- /dev/null +++ b/docs/library/layout/box.md @@ -0,0 +1,47 @@ +--- +components: + - rx.box +--- + +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +``` + +# Box + +Box is a generic container component that can be used to group other components. + +By default, the Box component is based on the `div` and rendered as a block element. It's primary use is for applying styles. + +## Basic Example + +```python demo +rx.box( + rx.box("CSS color", background_color="yellow", border_radius="2px", width="20%", margin="4px", padding="4px"), + rx.box("CSS color", background_color="orange", border_radius="5px", width="40%", margin="8px", padding="8px"), + rx.box("Radix Color", background_color="var(--tomato-3)", border_radius="5px", width="60%", margin="12px", padding="12px"), + rx.box("Radix Color", background_color="var(--plum-3)", border_radius="10px", width="80%", margin="16px", padding="16px"), + rx.box("Radix Theme Color", background_color="var(--accent-2)", radius="full", width="100%", margin="24px", padding="25px"), + flex_grow="1", + text_align="center", +) +``` + +## Background + +To set a background [image](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_images) or +[gradient](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_images/Using_CSS_gradients), +use the [`background` CSS prop](https://developer.mozilla.org/en-US/docs/Web/CSS/background). + +```python demo +rx.flex( + rx.box(background="linear-gradient(45deg, var(--tomato-9), var(--plum-9))", width="20%", height="100%"), + rx.box(background="linear-gradient(red, yellow, blue, orange)", width="20%", height="100%"), + rx.box(background="radial-gradient(at 0% 30%, red 10px, yellow 30%, #1e90ff 50%)", width="20%", height="100%"), + rx.box(background=f"center/cover url('{REFLEX_ASSETS_CDN}other/reflex_banner.png')", width="20%", height="100%"), + spacing="2", + width="100%", + height="10vh", +) +``` diff --git a/docs/library/layout/card.md b/docs/library/layout/card.md new file mode 100644 index 00000000000..78d70efaad2 --- /dev/null +++ b/docs/library/layout/card.md @@ -0,0 +1,59 @@ +--- +components: + - rx.card + +Card: | + lambda **props: rx.card("Basic Card ", **props) +--- + +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +``` + +# Card + +A Card component is used for grouping related components. It is similar to the Box, except it has a +border, uses the theme colors and border radius, and provides a `size` prop to control spacing +and margin according to the Radix `"1"` - `"5"` scale. + +The Card requires less styling than a Box to achieve consistent visual results when used with +themes. + +## Basic Example + +```python demo +rx.flex( + rx.card("Card 1", size="1"), + rx.card("Card 2", size="2"), + rx.card("Card 3", size="3"), + rx.card("Card 4", size="4"), + rx.card("Card 5", size="5"), + spacing="2", + align_items="flex-start", + flex_wrap="wrap", +) +``` + +## Rendering as a Different Element + +The `as_child` prop may be used to render the Card as a different element. Link and Button are +commonly used to make a Card clickable. + +```python demo +rx.card( + rx.link( + rx.flex( + rx.avatar(src=f"{REFLEX_ASSETS_CDN}other/reflex_banner.png"), + rx.box( + rx.heading("Quick Start"), + rx.text("Get started with Reflex in 5 minutes."), + ), + spacing="2", + ), + ), + as_child=True, +) +``` + +## Using Inset Content diff --git a/docs/library/layout/center.md b/docs/library/layout/center.md new file mode 100644 index 00000000000..3d422553680 --- /dev/null +++ b/docs/library/layout/center.md @@ -0,0 +1,21 @@ +--- +components: + - rx.center +--- + +```python exec +import reflex as rx +``` + +# Center + +`Center` is a component that centers its children within itself. It is based on the `flex` component and therefore inherits all of its props. + +```python demo +rx.center( + rx.text("Hello World!"), + border_radius="15px", + border_width="thick", + width="50%", +) +``` diff --git a/docs/library/layout/container.md b/docs/library/layout/container.md new file mode 100644 index 00000000000..caeab8c84a1 --- /dev/null +++ b/docs/library/layout/container.md @@ -0,0 +1,40 @@ +--- +components: + - rx.container +--- + +```python exec +import reflex as rx +``` + +# Container + +Constrains the maximum width of page content, while keeping flexible margins +for responsive layouts. + +A Container is generally used to wrap the main content for a page. + +## Basic Example + +```python demo +rx.box( + rx.container( + rx.card("This content is constrained to a max width of 448px.", width="100%"), + size="1", + ), + rx.container( + rx.card("This content is constrained to a max width of 688px.", width="100%"), + size="2", + ), + rx.container( + rx.card("This content is constrained to a max width of 880px.", width="100%"), + size="3", + ), + rx.container( + rx.card("This content is constrained to a max width of 1136px.", width="100%"), + size="4", + ), + background_color="var(--gray-3)", + width="100%", +) +``` diff --git a/docs/library/layout/flex.md b/docs/library/layout/flex.md new file mode 100644 index 00000000000..181cc5317ae --- /dev/null +++ b/docs/library/layout/flex.md @@ -0,0 +1,208 @@ +--- +components: + - rx.flex +--- + +```python exec +import reflex as rx +``` + +# Flex + +The Flex component is used to make [flexbox layouts](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Flexbox). +It makes it simple to arrange child components in horizontal or vertical directions, apply wrapping, +justify and align content, and automatically size components based on available space, making it +ideal for building responsive layouts. + +By default, children are arranged horizontally (`direction="row"`) without wrapping. + +## Basic Example + +```python demo +rx.flex( + rx.card("Card 1"), + rx.card("Card 2"), + rx.card("Card 3"), + rx.card("Card 4"), + rx.card("Card 5"), + spacing="2", + width="100%", +) +``` + +## Wrapping + +With `flex_wrap="wrap"`, the children will wrap to the next line instead of being resized. + +```python demo +rx.flex( + rx.foreach( + rx.Var.range(10), + lambda i: rx.card(f"Card {i + 1}", width="16%"), + ), + spacing="2", + flex_wrap="wrap", + width="100%", +) +``` + +## Direction + +With `direction="column"`, the children will be arranged vertically. + +```python demo +rx.flex( + rx.card("Card 1"), + rx.card("Card 2"), + rx.card("Card 3"), + rx.card("Card 4"), + spacing="2", + direction="column", +) +``` + +## Alignment + +Two props control how children are aligned within the Flex component: + +- `align` controls how children are aligned along the cross axis (vertical for `row` and horizontal for `column`). +- `justify` controls how children are aligned along the main axis (horizontal for `row` and vertical for `column`). + +The following example visually demonstrates the effect of these props with different `wrap` and `direction` values. + +```python demo exec +class FlexPlaygroundState(rx.State): + align: str = "stretch" + justify: str = "start" + direction: str = "row" + wrap: str = "nowrap" + + @rx.event + def set_align(self, value: str): + self.align = value + + @rx.event + def set_justify(self, value: str): + self.justify = value + + @rx.event + def set_direction(self, value: str): + self.direction = value + + @rx.event + def set_wrap(self, value: str): + self.wrap = value + + +def select(label, items, value, on_change): + return rx.flex( + rx.text(label), + rx.select.root( + rx.select.trigger(), + rx.select.content( + *[ + rx.select.item(item, value=item) + for item in items + ] + ), + value=value, + on_change=on_change, + ), + align="center", + justify="center", + direction="column", + ) + + +def selectors(): + return rx.flex( + select("Wrap", ["nowrap", "wrap", "wrap-reverse"], FlexPlaygroundState.wrap, FlexPlaygroundState.set_wrap), + select("Direction", ["row", "column", "row-reverse", "column-reverse"], FlexPlaygroundState.direction, FlexPlaygroundState.set_direction), + select("Align", ["start", "center", "end", "baseline", "stretch"], FlexPlaygroundState.align, FlexPlaygroundState.set_align), + select("Justify", ["start", "center", "end", "between"], FlexPlaygroundState.justify, FlexPlaygroundState.set_justify), + width="100%", + spacing="2", + justify="between", + ) + + +def example1(): + return rx.box( + selectors(), + rx.flex( + rx.foreach( + rx.Var.range(10), + lambda i: rx.card(f"Card {i + 1}", width="16%"), + ), + spacing="2", + direction=FlexPlaygroundState.direction, + align=FlexPlaygroundState.align, + justify=FlexPlaygroundState.justify, + wrap=FlexPlaygroundState.wrap, + width="100%", + height="20vh", + margin_top="16px", + ), + width="100%", + ) +``` + +## Size Hinting + +When a child component is included in a flex container, +the `flex_grow` (default `"0"`) and `flex_shrink` (default `"1"`) props control +how the box is sized relative to other components in the same container. + +The resizing always applies to the main axis of the flex container. If the direction is +`row`, then the sizing applies to the `width`. If the direction is `column`, then the sizing +applies to the `height`. To set the optimal size along the main axis, the `flex_basis` prop +is used and may be either a percentage or CSS size units. When unspecified, the +corresponding `width` or `height` value is used if set, otherwise the content size is used. + +When `flex_grow="0"`, the box will not grow beyond the `flex_basis`. + +When `flex_shrink="0"`, the box will not shrink to less than the `flex_basis`. + +These props are used when creating flexible responsive layouts. + +Move the slider below and see how adjusting the width of the flex container +affects the computed sizes of the flex items based on the props that are set. + +```python demo exec +class FlexGrowShrinkState(rx.State): + width_pct: list[int] = [100] + + @rx.event + def set_width_pct(self, value: list[int | float]): + self.width_pct = [int(value[0])] + + +def border_box(*children, **props): + return rx.box( + *children, + border="1px solid var(--gray-10)", + border_radius="2px", + **props, + ) + + +def example2(): + return rx.box( + rx.flex( + border_box("flex_shrink=0", flex_shrink="0", width="100px"), + border_box("flex_shrink=1", flex_shrink="1", width="200px"), + border_box("flex_grow=0", flex_grow="0"), + border_box("flex_grow=1", flex_grow="1"), + width=f"{FlexGrowShrinkState.width_pct}%", + margin_bottom="16px", + spacing="2", + ), + rx.slider( + min=0, + max=100, + value=FlexGrowShrinkState.width_pct, + on_change=FlexGrowShrinkState.set_width_pct, + ), + width="100%", + ) +``` diff --git a/docs/library/layout/fragment.md b/docs/library/layout/fragment.md new file mode 100644 index 00000000000..99f3ff2fc19 --- /dev/null +++ b/docs/library/layout/fragment.md @@ -0,0 +1,26 @@ +--- +components: + - rx.fragment +--- + +# Fragment + +```python exec +import reflex as rx +from pcweb import constants +``` + +A Fragment is a Component that allow you to group multiple Components without a wrapper node. + +Refer to the React docs at [React/Fragment]({constants.FRAGMENT_COMPONENT_INFO_URL}) for more information on its use-case. + +```python demo +rx.fragment( + rx.text("Component1"), + rx.text("Component2") +) +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=3196&end=3340 +# Video: Fragment +``` diff --git a/docs/library/layout/grid.md b/docs/library/layout/grid.md new file mode 100644 index 00000000000..f09071a2d71 --- /dev/null +++ b/docs/library/layout/grid.md @@ -0,0 +1,40 @@ +--- +components: + - rx.grid +--- + +```python exec +import reflex as rx +``` + +# Grid + +Component for creating grid layouts. Either `rows` or `columns` may be specified. + +## Basic Example + +```python demo +rx.grid( + rx.foreach( + rx.Var.range(12), + lambda i: rx.card(f"Card {i + 1}", height="10vh"), + ), + columns="3", + spacing="4", + width="100%", +) +``` + +```python demo +rx.grid( + rx.foreach( + rx.Var.range(12), + lambda i: rx.card(f"Card {i + 1}", height="10vh"), + ), + rows="3", + flow="column", + justify="between", + spacing="4", + width="100%", +) +``` diff --git a/docs/library/layout/inset.md b/docs/library/layout/inset.md new file mode 100644 index 00000000000..d70aa9fea74 --- /dev/null +++ b/docs/library/layout/inset.md @@ -0,0 +1,90 @@ +--- +components: + - rx.inset + +Inset: | + lambda **props: rx.card( + rx.inset( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/reflex_banner.png", height="auto"), + **props, + ), + width="500px", + ) +--- + +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +``` + +# Inset + +Applies a negative margin to allow content to bleed into the surrounding container. + +## Basic Example + +Nesting an Inset component inside a Card will render the content from edge to edge of the card. + +```python demo +rx.card( + rx.inset( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/reflex_banner.png", width="100%", height="auto"), + side="top", + pb="current", + ), + rx.text("Reflex is a web framework that allows developers to build their app in pure Python."), + width="25vw", +) +``` + +## Other Directions + +The `side` prop controls which side the negative margin is applied to. When using a specific side, +it is helpful to set the padding for the opposite side to `current` to retain the same padding the +content would have had if it went to the edge of the parent component. + +```python demo +rx.card( + rx.text("The inset below uses a bottom side."), + rx.inset( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/reflex_banner.png", width="100%", height="auto"), + side="bottom", + pt="current", + ), + width="25vw", +) +``` + +```python demo +rx.card( + rx.flex( + rx.text("This inset uses a right side, which requires a flex with direction row."), + rx.inset( + rx.box(background=f"center/cover url('{REFLEX_ASSETS_CDN}other/reflex_banner.png')", height="100%"), + width="100%", + side="right", + pl="current", + ), + direction="row", + width="100%", + ), + width="25vw", +) +``` + +```python demo +rx.card( + rx.flex( + rx.inset( + rx.box(background=f"center/cover url('{REFLEX_ASSETS_CDN}other/reflex_banner.png')", height="100%"), + width="100%", + side="left", + pr="current", + ), + rx.text("This inset uses a left side, which also requires a flex with direction row."), + direction="row", + width="100%", + ), + width="25vw", +) +``` diff --git a/docs/library/layout/section.md b/docs/library/layout/section.md new file mode 100644 index 00000000000..05f818bd0e1 --- /dev/null +++ b/docs/library/layout/section.md @@ -0,0 +1,36 @@ +--- +components: + - rx.section +--- + +```python exec +import reflex as rx +``` + +# Section + +Denotes a section of page content, providing vertical padding by default. + +Primarily this is a semantic component that is used to group related textual content. + +## Basic Example + +```python demo +rx.box( + rx.section( + rx.heading("First"), + rx.text("This is the first content section"), + padding_left="12px", + padding_right="12px", + background_color="var(--gray-2)", + ), + rx.section( + rx.heading("Second"), + rx.text("This is the second content section"), + padding_left="12px", + padding_right="12px", + background_color="var(--gray-2)", + ), + width="100%", +) +``` diff --git a/docs/library/layout/separator.md b/docs/library/layout/separator.md new file mode 100644 index 00000000000..e1d4e443399 --- /dev/null +++ b/docs/library/layout/separator.md @@ -0,0 +1,58 @@ +--- +components: + - rx.separator +Separator: | + lambda **props: rx.separator(**props) +--- + +```python exec +import reflex as rx +``` + +# Separator + +Visually or semantically separates content. + +## Basic Example + +```python demo +rx.flex( + rx.card("Section 1"), + rx.divider(), + rx.card("Section 2"), + spacing="4", + direction="column", + align="center", +) +``` + +## Size + +The `size` prop controls how long the separator is. Using `size="4"` will make +the separator fill the parent container. Setting CSS `width` or `height` prop to `"100%"` +can also achieve this effect, but `size` works the same regardless of the orientation. + +```python demo +rx.flex( + rx.card("Section 1"), + rx.divider(size="4"), + rx.card("Section 2"), + spacing="4", + direction="column", +) +``` + +## Orientation + +Setting the orientation prop to `vertical` will make the separator appear vertically. + +```python demo +rx.flex( + rx.card("Section 1"), + rx.divider(orientation="vertical", size="4"), + rx.card("Section 2"), + spacing="4", + width="100%", + height="10vh", +) +``` diff --git a/docs/library/layout/spacer.md b/docs/library/layout/spacer.md new file mode 100644 index 00000000000..0d9d1b01723 --- /dev/null +++ b/docs/library/layout/spacer.md @@ -0,0 +1,25 @@ +--- +components: + - rx.spacer +--- + +```python exec +import reflex as rx +``` + +# Spacer + +Creates an adjustable, empty space that can be used to tune the spacing between child elements within `flex`. + +```python demo +rx.flex( + rx.center(rx.text("Example"), bg="lightblue"), + rx.spacer(), + rx.center(rx.text("Example"), bg="lightgreen"), + rx.spacer(), + rx.center(rx.text("Example"), bg="salmon"), + width="100%", +) +``` + +As `stack`, `vstack` and `hstack` are all built from `flex`, it is possible to also use `spacer` inside of these components. diff --git a/docs/library/layout/stack.md b/docs/library/layout/stack.md new file mode 100644 index 00000000000..04fdde0b292 --- /dev/null +++ b/docs/library/layout/stack.md @@ -0,0 +1,172 @@ +--- +components: + - rx.stack + - rx.hstack + - rx.vstack +Stack: | + lambda **props: rx.stack( + rx.card("Card 1", size="2"), rx.card("Card 2", size="2"), rx.card("Card 3", size="2"), + width="100%", + height="20vh", + **props, + ) +--- + +```python exec +import reflex as rx +``` + +# Stack + +`Stack` is a layout component used to group elements together and apply a space between them. + +`vstack` is used to stack elements in the vertical direction. + +`hstack` is used to stack elements in the horizontal direction. + +`stack` is used to stack elements in the vertical or horizontal direction. + +These components are based on the `flex` component and therefore inherit all of its props. + +The `stack` component can be used with the `flex_direction` prop to set to either `row` or `column` to set the direction. + +```python demo +rx.flex( + rx.stack( + rx.box( + "Example", + bg="orange", + border_radius="3px", + width="20%", + ), + rx.box( + "Example", + bg="lightblue", + border_radius="3px", + width="30%", + ), + rx.box( + "Example", + bg="lightgreen", + border_radius="3px", + width="50%", + ), + flex_direction="row", + width="100%", + ), + rx.stack( + rx.box( + "Example", + bg="orange", + border_radius="3px", + width="20%", + ), + rx.box( + "Example", + bg="lightblue", + border_radius="3px", + width="30%", + ), + rx.box( + "Example", + bg="lightgreen", + border_radius="3px", + width="50%", + ), + flex_direction="column", + width="100%", + ), + width="100%", +) +``` + +## Hstack + +```python demo +rx.hstack( + rx.box( + "Example", bg="red", border_radius="3px", width="10%" + ), + rx.box( + "Example", + bg="orange", + border_radius="3px", + width="10%", + ), + rx.box( + "Example", + bg="yellow", + border_radius="3px", + width="10%", + ), + rx.box( + "Example", + bg="lightblue", + border_radius="3px", + width="10%", + ), + rx.box( + "Example", + bg="lightgreen", + border_radius="3px", + width="60%", + ), + width="100%", +) +``` + +## Vstack + +```python demo +rx.vstack( + rx.box( + "Example", bg="red", border_radius="3px", width="20%" + ), + rx.box( + "Example", + bg="orange", + border_radius="3px", + width="40%", + ), + rx.box( + "Example", + bg="yellow", + border_radius="3px", + width="60%", + ), + rx.box( + "Example", + bg="lightblue", + border_radius="3px", + width="80%", + ), + rx.box( + "Example", + bg="lightgreen", + border_radius="3px", + width="100%", + ), + width="100%", +) +``` + +## Real World Example + +```python demo +rx.hstack( + rx.box( + rx.heading("Saving Money"), + rx.text("Saving money is an art that combines discipline, strategic planning, and the wisdom to foresee future needs and emergencies. It begins with the simple act of setting aside a portion of one's income, creating a buffer that can grow over time through interest or investments.", margin_top="0.5em"), + padding="1em", + border_width="1px", + ), + rx.box( + rx.heading("Spending Money"), + rx.text("Spending money is a balancing act between fulfilling immediate desires and maintaining long-term financial health. It's about making choices, sometimes indulging in the pleasures of the moment, and at other times, prioritizing essential expenses.", margin_top="0.5em"), + padding="1em", + border_width="1px", + ), + gap="2em", +) + +``` diff --git a/docs/library/media/audio.md b/docs/library/media/audio.md new file mode 100644 index 00000000000..d3f8e8ebeaf --- /dev/null +++ b/docs/library/media/audio.md @@ -0,0 +1,29 @@ +--- +components: + - rx.audio +--- + +# Audio + +```python exec +import reflex as rx +from pcweb.pages.docs import library +``` + +The audio component can display an audio given an src path as an argument. This could either be a local path from the assets folder or an external link. + +```python demo +rx.audio( + src="https://www.learningcontainer.com/wp-content/uploads/2020/02/Kalimba.mp3", + width="400px", + height="32px", +) +``` + +If we had a local file in the `assets` folder named `test.mp3` we could set `src="/test.mp3"` to view the audio file. + +```md alert info +# How to let your user upload an audio file + +To let a user upload an audio file to your app check out the [upload docs]({library.forms.upload.path}). +``` diff --git a/docs/library/media/image.md b/docs/library/media/image.md new file mode 100644 index 00000000000..5640f1c230a --- /dev/null +++ b/docs/library/media/image.md @@ -0,0 +1,65 @@ +--- +components: + - rx.image +--- + +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +from pcweb.pages.docs import library +``` + +# Image + +The Image component can display an image given a `src` path as an argument. +This could either be a local path from the assets folder or an external link. + +```python demo +rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="100px", height="auto") +``` + +Image composes a box and can be styled similarly. + +```python demo +rx.image( + src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", + width="100px", + height="auto", + border_radius="15px 50px", + border="5px solid #555", +) +``` + +You can also pass a `PIL` image object as the `src`. + +```python demo box +rx.image(src="https://picsum.photos/id/1/200/300", alt="An Unsplash Image") +``` + +```python +from PIL import Image +import requests + + +class ImageState(rx.State): + url: str = f"https://picsum.photos/id/1/200/300" + image: Image.Image = Image.open(requests.get(url, stream=True).raw) + + +def image_pil_example(): + return rx.vstack( + rx.image(src=ImageState.image) + ) +``` + +```md alert info +# rx.image only accepts URLs and Pillow Images + +A cv2 image must be converted to a PIL image to be passed directly to `rx.image` as a State variable, or saved to the `assets` folder and then passed to the `rx.image` component. +``` + +```md alert info +# How to let your user upload an image + +To let a user upload an image to your app check out the [upload docs]({library.forms.upload.path}). +``` diff --git a/docs/library/media/video.md b/docs/library/media/video.md new file mode 100644 index 00000000000..668e00ba1d9 --- /dev/null +++ b/docs/library/media/video.md @@ -0,0 +1,29 @@ +--- +components: + - rx.video +--- + +# Video + +```python exec +import reflex as rx +from pcweb.pages.docs import library +``` + +The video component can display a video given an src path as an argument. This could either be a local path from the assets folder or an external link. + +```python demo +rx.video( + src="https://www.youtube.com/embed/9bZkp7q19f0", + width="400px", + height="auto" +) +``` + +If we had a local file in the `assets` folder named `test.mp4` we could set `url="/test.mp4"` to view the video. + +```md alert info +# How to let your user upload a video + +To let a user upload a video to your app check out the [upload docs]({library.forms.upload.path}). +``` diff --git a/docs/library/other/clipboard.md b/docs/library/other/clipboard.md new file mode 100644 index 00000000000..061dc5d33a0 --- /dev/null +++ b/docs/library/other/clipboard.md @@ -0,0 +1,79 @@ +--- +components: + - rx.clipboard +--- + +```python exec +import reflex as rx +from pcweb.pages.docs import styling +``` + +# Clipboard + +_New in 0.5.6_ + +The Clipboard component can be used to respond to paste events with complex data. + +If the Clipboard component is included in a page without children, +`rx.clipboard()`, then it will attach to the document's `paste` event handler +and will be triggered when data is pasted anywhere into the page. + +```python demo exec +class ClipboardPasteState(rx.State): + @rx.event + def on_paste(self, data: list[tuple[str, str]]): + for mime_type, item in data: + yield rx.toast(f"Pasted {mime_type} data: {item}") + + +def clipboard_example(): + return rx.fragment( + rx.clipboard(on_paste=ClipboardPasteState.on_paste), + "Paste Content Here", + ) +``` + +The `data` argument passed to the `on_paste` method is a list of tuples, where +each tuple contains the MIME type of the pasted data and the data itself. Binary +data will be base64 encoded as a data URI, and can be decoded using python's +`urlopen` or used directly as the `src` prop of an image. + +## Scoped Paste Events + +If you want to limit the scope of the paste event to a specific element, wrap +the `rx.clipboard` component around the elements that should trigger the paste +event. + +To avoid having outer paste handlers also trigger the event, you can use the +event action `.stop_propagation` to prevent the paste from bubbling up through +the DOM. + +If you need to also prevent the default action of pasting the data into a text +box, you can also attach the `.prevent_default` action. + +```python demo exec +class ClipboardPasteImageState(rx.State): + last_image_uri: str = "" + + def on_paste(self, data: list[tuple[str, str]]): + for mime_type, item in data: + if mime_type.startswith("image/"): + self.last_image_uri = item + break + else: + return rx.toast("Did not find an image in the pasted data") + + +def clipboard_image_example(): + return rx.vstack( + rx.clipboard( + rx.input(placeholder="Paste Image (stop propagation)"), + on_paste=ClipboardPasteImageState.on_paste.stop_propagation + ), + rx.clipboard( + rx.input(placeholder="Paste Image (prevent default)"), + on_paste=ClipboardPasteImageState.on_paste.prevent_default + ), + rx.image(src=ClipboardPasteImageState.last_image_uri), + ) +``` diff --git a/docs/library/other/html.md b/docs/library/other/html.md new file mode 100644 index 00000000000..f74ec858c9d --- /dev/null +++ b/docs/library/other/html.md @@ -0,0 +1,119 @@ +--- +components: + - rx.el.a + - rx.el.abbr + - rx.el.address + - rx.el.area + - rx.el.article + - rx.el.aside + - rx.el.audio + - rx.el.b + - rx.el.bdi + - rx.el.bdo + - rx.el.blockquote + - rx.el.body + - rx.el.br + - rx.el.button + - rx.el.canvas + - rx.el.caption + - rx.el.cite + - rx.el.code + - rx.el.col + - rx.el.colgroup + - rx.el.data + - rx.el.dd + - rx.el.Del + - rx.el.details + - rx.el.dfn + - rx.el.dialog + - rx.el.div + - rx.el.dl + - rx.el.dt + - rx.el.em + - rx.el.embed + - rx.el.fieldset + - rx.el.figcaption + - rx.el.footer + - rx.el.form + - rx.el.h1 + - rx.el.h2 + - rx.el.h3 + - rx.el.h4 + - rx.el.h5 + - rx.el.h6 + - rx.el.head + - rx.el.header + - rx.el.hr + - rx.el.html + - rx.el.i + - rx.el.iframe + - rx.el.img + - rx.el.input + - rx.el.ins + - rx.el.kbd + - rx.el.label + - rx.el.legend + - rx.el.li + - rx.el.link + - rx.el.main + - rx.el.mark + - rx.el.math + - rx.el.meta + - rx.el.meter + - rx.el.nav + - rx.el.noscript + - rx.el.object + - rx.el.ol + - rx.el.optgroup + - rx.el.option + - rx.el.output + - rx.el.p + - rx.el.picture + - rx.el.portal + - rx.el.pre + - rx.el.progress + - rx.el.q + - rx.el.rp + - rx.el.rt + - rx.el.ruby + - rx.el.s + - rx.el.samp + - rx.el.script + - rx.el.section + - rx.el.select + - rx.el.small + - rx.el.source + - rx.el.span + - rx.el.strong + - rx.el.sub + - rx.el.sup + - rx.el.svg.circle + - rx.el.svg.defs + - rx.el.svg.linear_gradient + - rx.el.svg.polygon + - rx.el.svg.path + - rx.el.svg.rect + - rx.el.svg.stop + - rx.el.table + - rx.el.tbody + - rx.el.td + - rx.el.template + - rx.el.textarea + - rx.el.tfoot + - rx.el.th + - rx.el.thead + - rx.el.time + - rx.el.title + - rx.el.tr + - rx.el.track + - rx.el.u + - rx.el.ul + - rx.el.video + - rx.el.wbr +--- + +# HTML + +Reflex also provides a set of HTML elements that can be used to create web pages. These elements are the same as the HTML elements that are used in web development. These elements come unstyled bhy default. You can style them using style props or tailwindcss classes. + +The following is a list of the HTML elements that are available in Reflex: diff --git a/docs/library/other/html_embed.md b/docs/library/other/html_embed.md new file mode 100644 index 00000000000..f3ff0916823 --- /dev/null +++ b/docs/library/other/html_embed.md @@ -0,0 +1,39 @@ +--- +components: + - rx.html +--- + +```python exec +import reflex as rx +from pcweb.pages.docs import styling +``` + +# HTML Embed + +The HTML component can be used to render raw HTML code. + +Before you reach for this component, consider using Reflex's raw HTML element support instead. + +```python demo +rx.vstack( + rx.html("

Hello World

"), + rx.html("

Hello World

"), + rx.html("

Hello World

"), + rx.html("

Hello World

"), + rx.html("
Hello World
"), + rx.html("
Hello World
"), +) +``` + +```md alert +# Missing Styles? + +Reflex uses Radix-UI and tailwind for styling, both of which reset default styles for headings. +If you are using the html component and want pretty default styles, consider setting `class_name='prose'`, adding `@tailwindcss/typography` package to `frontend_packages` and enabling it via `tailwind` config in `rxconfig.py`. See the [Tailwind docs]({styling.overview.path}) for an example of adding this plugin. +``` + +In this example, we render an image. + +```python demo +rx.html("") +``` diff --git a/docs/library/other/memo.md b/docs/library/other/memo.md new file mode 100644 index 00000000000..83ee4306bfd --- /dev/null +++ b/docs/library/other/memo.md @@ -0,0 +1,167 @@ +```python exec +import reflex as rx +from pcweb.pages.docs import styling +``` + +# Memo + +The `memo` decorator is used to optimize component rendering by memoizing components that don't need to be re-rendered. This is particularly useful for expensive components that depend on specific props and don't need to be re-rendered when other state changes in your application. + +## Requirements + +When using `rx.memo`, you must follow these requirements: + +1. **Type all arguments**: All arguments to a memoized component must have type annotations. +2. **Use keyword arguments**: When calling a memoized component, you must use keyword arguments (not positional arguments). + +## Basic Usage + +When you wrap a component function with `@rx.memo`, the component will only re-render when its props change. This helps improve performance by preventing unnecessary re-renders. + +```python +# Define a state class to track count +class DemoState(rx.State): + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + +# Define a memoized component +@rx.memo +def expensive_component(label: str) -> rx.Component: + return rx.vstack( + rx.heading(label), + rx.text("This component only re-renders when props change!"), + rx.divider(), + ) + +# Use the memoized component in your app +def index(): + return rx.vstack( + rx.heading("Memo Example"), + rx.text("Count: 0"), # This will update with state.count + rx.button("Increment", on_click=DemoState.increment), + rx.divider(), + expensive_component(label="Memoized Component"), # Must use keyword arguments + spacing="4", + padding="4", + border_radius="md", + border="1px solid #eaeaea", + ) +``` + +In this example, the `expensive_component` will only re-render when the `label` prop changes, not when the `count` state changes. + +## With Event Handlers + +You can also use `rx.memo` with components that have event handlers: + +```python +# Define a state class to track clicks +class ButtonState(rx.State): + clicks: int = 0 + + @rx.event + def increment(self): + self.clicks += 1 + +# Define a memoized button component +@rx.memo +def my_button(text: str, on_click: rx.EventHandler) -> rx.Component: + return rx.button(text, on_click=on_click) + +# Use the memoized button in your app +def index(): + return rx.vstack( + rx.text("Clicks: 0"), # This will update with state.clicks + my_button( + text="Click me", + on_click=ButtonState.increment + ), + spacing="4", + ) +``` + +## With State Variables + +When used with state variables, memoized components will only re-render when the specific state variables they depend on change: + +```python +# Define a state class with multiple variables +class AppState(rx.State): + name: str = "World" + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + + @rx.event + def set_name(self, name: str): + self.name = name + +# Define a memoized greeting component +@rx.memo +def greeting(name: str) -> rx.Component: + return rx.heading("Hello, " + name) # Will display the name prop + +# Use the memoized component with state variables +def index(): + return rx.vstack( + greeting(name=AppState.name), # Must use keyword arguments + rx.text("Count: 0"), # Will display the count + rx.button("Increment Count", on_click=AppState.increment), + rx.input( + placeholder="Enter your name", + on_change=AppState.set_name, + value="World", # Will be bound to AppState.name + ), + spacing="4", + ) +``` + +## Advanced Event Handler Example + +You can also pass arguments to event handlers in memoized components: + +```python +# Define a state class to track messages +class MessageState(rx.State): + message: str = "" + + @rx.event + def set_message(self, text: str): + self.message = text + +# Define a memoized component with event handlers that pass arguments +@rx.memo +def action_buttons(on_action: rx.EventHandler[rx.event.passthrough_event_spec(str)]) -> rx.Component: + return rx.hstack( + rx.button("Save", on_click=on_action("Saved!")), + rx.button("Delete", on_click=on_action("Deleted!")), + rx.button("Cancel", on_click=on_action("Cancelled!")), + spacing="2", + ) + +# Use the memoized component with event handlers +def index(): + return rx.vstack( + rx.text("Status: "), # Will display the message + action_buttons(on_action=MessageState.set_message), + spacing="4", + ) +``` + +## Performance Considerations + +Use `rx.memo` for: + +- Components with expensive rendering logic +- Components that render the same result given the same props +- Components that re-render too often due to parent component updates + +Avoid using `rx.memo` for: + +- Simple components where the memoization overhead might exceed the performance gain +- Components that almost always receive different props on re-render diff --git a/docs/library/other/script.md b/docs/library/other/script.md new file mode 100644 index 00000000000..8065ad34c67 --- /dev/null +++ b/docs/library/other/script.md @@ -0,0 +1,42 @@ +--- +components: + - rx.script +--- + +```python exec +import reflex as rx +``` + +# Script + +The Script component can be used to include inline javascript or javascript files by URL. + +It uses the [`next/script` component](https://nextjs.org/docs/app/api-reference/components/script) to inject the script and can be safely used with conditional rendering to allow script side effects to be controlled by the state. + +```python +rx.script("console.log('inline javascript')") +``` + +Complex inline scripting should be avoided. +If the code to be included is more than a couple lines, it is more maintainable to implement it in a separate javascript file in the `assets` directory and include it via the `src` prop. + +```python +rx.script(src="/my-custom.js") +``` + +This component is particularly helpful for including tracking and social scripts. +Any additional attrs needed for the script tag can be supplied via `custom_attrs` prop. + +```python +rx.script(src="//gc.zgo.at/count.js", custom_attrs=\{"data-goatcounter": "https://reflextoys.goatcounter.com/count"}) +``` + +This code renders to something like the following to enable stat counting with a third party service. + +```jsx + +``` diff --git a/docs/library/other/skeleton.md b/docs/library/other/skeleton.md new file mode 100644 index 00000000000..1f6c1927f0f --- /dev/null +++ b/docs/library/other/skeleton.md @@ -0,0 +1,27 @@ +--- +description: Skeleton, a loading placeholder component for content that is not yet available. +components: + - rx.skeleton +--- + +```python exec +import reflex as rx +``` + +# Skeleton (loading placeholder) + +`Skeleton` is a loading placeholder component that serves as a visual placeholder while content is loading. +It is useful for maintaining the layout's structure and providing users with a sense of progression while awaiting the final content. + +```python demo +rx.vstack( + rx.skeleton(rx.button("button-small"), height="10px"), + rx.skeleton(rx.button("button-big"), height="20px"), + rx.skeleton(rx.text("Text is loaded."), loading=True,), + rx.skeleton(rx.text("Text is already loaded."), loading=False,), +), +``` + +When using `Skeleton` with text, wrap the text itself instead of the parent element to have a placeholder of the same size. + +Use the loading prop to control whether the skeleton or its children are displayed. Skeleton preserves the dimensions of children when they are hidden and disables interactive elements. diff --git a/docs/library/other/theme.md b/docs/library/other/theme.md new file mode 100644 index 00000000000..3683b2cc061 --- /dev/null +++ b/docs/library/other/theme.md @@ -0,0 +1,31 @@ +--- +components: + - rx.theme + - rx.theme_panel +--- + +# Theme + +The `Theme` component is used to change the theme of the application. The `Theme` can be set directly in the rx.App. + +```python +app = rx.App( + theme=rx.theme( + appearance="light", has_background=True, radius="large", accent_color="teal" + ) +) +``` + +# Theme Panel + +The `ThemePanel` component is a container for the `Theme` component. It provides a way to change the theme of the application. + +```python +rx.theme_panel() +``` + +The theme panel is closed by default. You can set it open `default_open=True`. + +```python +rx.theme_panel(default_open=True) +``` diff --git a/docs/library/overlay/alert_dialog.md b/docs/library/overlay/alert_dialog.md new file mode 100644 index 00000000000..7b4abb8b6fd --- /dev/null +++ b/docs/library/overlay/alert_dialog.md @@ -0,0 +1,366 @@ +--- +components: + - rx.alert_dialog.root + - rx.alert_dialog.content + - rx.alert_dialog.trigger + - rx.alert_dialog.title + - rx.alert_dialog.description + - rx.alert_dialog.action + - rx.alert_dialog.cancel + +only_low_level: + - True + +AlertDialogRoot: | + lambda **props: rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button("Revoke access"), + ), + rx.alert_dialog.content( + rx.alert_dialog.title("Revoke access"), + rx.alert_dialog.description( + "Are you sure? This application will no longer be accessible and any existing sessions will be expired.", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel"), + ), + rx.alert_dialog.action( + rx.button("Revoke access"), + ), + spacing="3", + ), + ), + **props + ) + +AlertDialogContent: | + lambda **props: rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button("Revoke access"), + ), + rx.alert_dialog.content( + rx.alert_dialog.title("Revoke access"), + rx.alert_dialog.description( + "Are you sure? This application will no longer be accessible and any existing sessions will be expired.", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel"), + ), + rx.alert_dialog.action( + rx.button("Revoke access"), + ), + spacing="3", + ), + **props + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Alert Dialog + +An alert dialog is a modal confirmation dialog that interrupts the user and expects a response. + +The `alert_dialog.root` contains all the parts of the dialog. + +The `alert_dialog.trigger` wraps the control that will open the dialog. + +The `alert_dialog.content` contains the content of the dialog. + +The `alert_dialog.title` is the title that is announced when the dialog is opened. + +The `alert_dialog.description` is an optional description that is announced when the dialog is opened. + +The `alert_dialog.action` wraps the control that will close the dialog. This should be distinguished visually from the `alert_dialog.cancel` control. + +The `alert_dialog.cancel` wraps the control that will close the dialog. This should be distinguished visually from the `alert_dialog.action` control. + +## Basic Example + +```python demo +rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button("Revoke access"), + ), + rx.alert_dialog.content( + rx.alert_dialog.title("Revoke access"), + rx.alert_dialog.description( + "Are you sure? This application will no longer be accessible and any existing sessions will be expired.", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel"), + ), + rx.alert_dialog.action( + rx.button("Revoke access"), + ), + spacing="3", + ), + ), +) +``` + +This example has a different color scheme and the `cancel` and `action` buttons are right aligned. + +```python demo +rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button("Revoke access", color_scheme="red"), + ), + rx.alert_dialog.content( + rx.alert_dialog.title("Revoke access"), + rx.alert_dialog.description( + "Are you sure? This application will no longer be accessible and any existing sessions will be expired.", + size="2", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel", variant="soft", color_scheme="gray"), + ), + rx.alert_dialog.action( + rx.button("Revoke access", color_scheme="red", variant="solid"), + ), + spacing="3", + margin_top="16px", + justify="end", + ), + style={"max_width": 450}, + ), +) +``` + +Use the `inset` component to align content flush with the sides of the dialog. + +```python demo +rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button("Delete Users", color_scheme="red"), + ), + rx.alert_dialog.content( + rx.alert_dialog.title("Delete Users"), + rx.alert_dialog.description( + "Are you sure you want to delete these users? This action is permanent and cannot be undone.", + size="2", + ), + rx.inset( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + ), + ), + ), + side="x", + margin_top="24px", + margin_bottom="24px", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel", variant="soft", color_scheme="gray"), + ), + rx.alert_dialog.action( + rx.button("Delete users", color_scheme="red"), + ), + spacing="3", + justify="end", + ), + style={"max_width": 500}, + ), +) +``` + +## Events when the Alert Dialog opens or closes + +The `on_open_change` event is called when the `open` state of the dialog changes. It is used in conjunction with the `open` prop. + +```python demo exec +class AlertDialogState(rx.State): + num_opens: int = 0 + opened: bool = False + + @rx.event + def count_opens(self, value: bool): + self.opened = value + self.num_opens += 1 + + +def alert_dialog(): + return rx.flex( + rx.heading(f"Number of times alert dialog opened or closed: {AlertDialogState.num_opens}"), + rx.heading(f"Alert Dialog open: {AlertDialogState.opened}"), + rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button("Revoke access", color_scheme="red"), + ), + rx.alert_dialog.content( + rx.alert_dialog.title("Revoke access"), + rx.alert_dialog.description( + "Are you sure? This application will no longer be accessible and any existing sessions will be expired.", + size="2", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel", variant="soft", color_scheme="gray"), + ), + rx.alert_dialog.action( + rx.button("Revoke access", color_scheme="red", variant="solid"), + ), + spacing="3", + margin_top="16px", + justify="end", + ), + style={"max_width": 450}, + ), + on_open_change=AlertDialogState.count_opens, + ), + direction="column", + spacing="3", + ) +``` + +## Controlling Alert Dialog with State + +This example shows how to control whether the dialog is open or not with state. This is an easy way to show the dialog without needing to use the `rx.alert_dialog.trigger`. + +`rx.alert_dialog.root` has a prop `open` that can be set to a boolean value to control whether the dialog is open or not. + +We toggle this `open` prop with a button outside of the dialog and the `rx.alert_dialog.cancel` and `rx.alert_dialog.action` buttons inside the dialog. + +```python demo exec +class AlertDialogState2(rx.State): + opened: bool = False + + @rx.event + def dialog_open(self): + self.opened = ~self.opened + + +def alert_dialog2(): + return rx.box( + rx.alert_dialog.root( + rx.alert_dialog.content( + rx.alert_dialog.title("Revoke access"), + rx.alert_dialog.description( + "Are you sure? This application will no longer be accessible and any existing sessions will be expired.", + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button("Cancel", on_click=AlertDialogState2.dialog_open), + ), + rx.alert_dialog.action( + rx.button("Revoke access", on_click=AlertDialogState2.dialog_open), + ), + spacing="3", + ), + ), + open=AlertDialogState2.opened, + ), + rx.button("Button to Open the Dialog", on_click=AlertDialogState2.dialog_open), +) +``` + +## Form Submission to a Database from an Alert Dialog + +This example adds new users to a database from an alert dialog using a form. + +1. It defines a User1 model with name and email fields. +2. The `add_user_to_db` method adds a new user to the database, checking for existing emails. +3. On form submission, it calls the `add_user_to_db` method. +4. The UI component has: + +- A button to open an alert dialog +- An alert dialog containing a form to add a new user +- Input fields for name and email +- Submit and Cancel buttons + +```python demo exec +class User1(rx.Model, table=True): + """The user model.""" + name: str + email: str + +class State(rx.State): + + current_user: User1 = User1() + + @rx.event + def add_user_to_db(self, form_data: dict): + self.current_user = form_data + ### Uncomment the code below to add your data to a database ### + # with rx.session() as session: + # if session.exec( + # select(User1).where(user.email == self.current_user["email"]) + # ).first(): + # return rx.window_alert("User with this email already exists") + # session.add(User1(**self.current_user)) + # session.commit() + + return rx.toast.info(f"User {self.current_user['name']} has been added.", position="bottom-right") + + +def index() -> rx.Component: + return rx.alert_dialog.root( + rx.alert_dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.alert_dialog.content( + rx.alert_dialog.title( + "Add New User", + ), + rx.alert_dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name" + ), + rx.input( + placeholder="user@reflex.dev", name="email" + ), + rx.flex( + rx.alert_dialog.cancel( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.alert_dialog.action( + rx.button("Submit", type="submit"), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State.add_user_to_db, + reset_on_submit=False, + ), + max_width="450px", + ), + ) +``` diff --git a/docs/library/overlay/context_menu.md b/docs/library/overlay/context_menu.md new file mode 100644 index 00000000000..b284a82986c --- /dev/null +++ b/docs/library/overlay/context_menu.md @@ -0,0 +1,343 @@ +--- +components: + - rx.context_menu.root + - rx.context_menu.item + - rx.context_menu.separator + - rx.context_menu.trigger + - rx.context_menu.content + - rx.context_menu.sub + - rx.context_menu.sub_trigger + - rx.context_menu.sub_content + +only_low_level: + - True + +ContextMenuRoot: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)") + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C"), + rx.context_menu.item("Share"), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate"), + rx.context_menu.item("Duplicate"), + rx.context_menu.item("Archive"), + ), + ), + ), + **props + ) + +ContextMenuTrigger: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)"), + **props + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C"), + rx.context_menu.item("Share"), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate"), + rx.context_menu.item("Duplicate"), + rx.context_menu.item("Archive"), + ), + ), + ), + ) + +ContextMenuContent: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)") + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C"), + rx.context_menu.item("Share"), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate"), + rx.context_menu.item("Duplicate"), + rx.context_menu.item("Archive"), + ), + ), + **props + ), + ) + +ContextMenuSub: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)") + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C"), + rx.context_menu.item("Share"), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate"), + rx.context_menu.item("Duplicate"), + rx.context_menu.item("Archive"), + ), + **props + ), + ), + ) + +ContextMenuSubTrigger: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)") + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C"), + rx.context_menu.item("Share"), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More", **props), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate"), + rx.context_menu.item("Duplicate"), + rx.context_menu.item("Archive"), + ), + ), + ), + ) + +ContextMenuSubContent: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)") + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C"), + rx.context_menu.item("Share"), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate"), + rx.context_menu.item("Duplicate"), + rx.context_menu.item("Archive"), + **props + ), + ), + ), + ) + +ContextMenuItem: | + lambda **props: rx.context_menu.root( + rx.context_menu.trigger( + rx.text("Context Menu (right click)") + ), + rx.context_menu.content( + rx.context_menu.item("Copy", shortcut="⌘ C", **props), + rx.context_menu.item("Share", **props), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red", **props), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Eradicate", **props), + rx.context_menu.item("Duplicate", **props), + rx.context_menu.item("Archive", **props), + ), + ), + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Context Menu + +A Context Menu is a popup menu that appears upon user interaction, such as a right-click or a hover. + +## Basic Usage + +A Context Menu is composed of a `context_menu.root`, a `context_menu.trigger` and a `context_menu.content`. The `context_menu_root` contains all the parts of a context menu. The `context_menu.trigger` is the element that the user interacts with to open the menu. It wraps the element that will open the context menu. The `context_menu.content` is the component that pops out when the context menu is open. + +The `context_menu.item` contains the actual context menu items and sits under the `context_menu.content`. + +The `context_menu.sub` contains all the parts of a submenu. There is a `context_menu.sub_trigger`, which is an item that opens a submenu. It must be rendered inside a `context_menu.sub` component. The `context_menu.sub_content` is the component that pops out when a submenu is open. It must also be rendered inside a `context_menu.sub` component. + +The `context_menu.separator` is used to visually separate items in a context menu. + +```python demo +rx.context_menu.root( + rx.context_menu.trigger( + rx.button("Right click me"), + ), + rx.context_menu.content( + rx.context_menu.item("Edit", shortcut="⌘ E"), + rx.context_menu.item("Duplicate", shortcut="⌘ D"), + rx.context_menu.separator(), + rx.context_menu.item("Archive", shortcut="⌘ N"), + rx.context_menu.sub( + rx.context_menu.sub_trigger("More"), + rx.context_menu.sub_content( + rx.context_menu.item("Move to project…"), + rx.context_menu.item("Move to folder…"), + rx.context_menu.separator(), + rx.context_menu.item("Advanced options…"), + ), + ), + rx.context_menu.separator(), + rx.context_menu.item("Share"), + rx.context_menu.item("Add to favorites"), + rx.context_menu.separator(), + rx.context_menu.item("Delete", shortcut="⌘ ⌫", color="red"), + ), +) +``` + +````md alert warning +# `rx.context_menu.item` must be a DIRECT child of `rx.context_menu.content` + +The code below for example is not allowed: + +```python +rx.context_menu.root( + rx.context_menu.trigger( + rx.button("Right click me"), + ), + rx.context_menu.content( + rx.cond( + State.count % 2 == 0, + rx.vstack( + rx.context_menu.item("Even Option 1", on_click=State.set_selected_option("Even Option 1")), + rx.context_menu.item("Even Option 2", on_click=State.set_selected_option("Even Option 2")), + rx.context_menu.item("Even Option 3", on_click=State.set_selected_option("Even Option 3")), + ), + rx.vstack( + rx.context_menu.item("Odd Option A", on_click=State.set_selected_option("Odd Option A")), + rx.context_menu.item("Odd Option B", on_click=State.set_selected_option("Odd Option B")), + rx.context_menu.item("Odd Option C", on_click=State.set_selected_option("Odd Option C")), + ) + ) + ), +) +``` +```` + +## Opening a Dialog from Context Menu using State + +Accessing an overlay component from within another overlay component is a common use case but does not always work exactly as expected. + +The code below will not work as expected as because the dialog is within the menu and the dialog will only be open when the menu is open, rendering the dialog unusable. + +```python +rx.context_menu.root( + rx.context_menu.trigger(rx.icon("ellipsis-vertical")), + rx.context_menu.content( + rx.context_menu.item( + rx.dialog.root( + rx.dialog.trigger(rx.text("Edit")), + rx.dialog.content(....), + ..... + ), + ), + ), +) +``` + +In this example, we will show how to open a dialog box from a context menu, where the menu will close and the dialog will open and be functional. + +```python demo exec +class ContextMenuState(rx.State): + which_dialog_open: str = "" + + @rx.event + def set_which_dialog_open(self, value: str): + self.which_dialog_open = value + + @rx.event + def delete(self): + yield rx.toast("Deleted item") + + @rx.event + def save_settings(self): + yield rx.toast("Saved settings") + + +def delete_dialog(): + return rx.alert_dialog.root( + rx.alert_dialog.content( + rx.alert_dialog.title("Are you Sure?"), + rx.alert_dialog.description( + rx.text( + "This action cannot be undone. Are you sure you want to delete this item?", + ), + margin_bottom="20px", + ), + rx.hstack( + rx.alert_dialog.action( + rx.button( + "Delete", + color_scheme="red", + on_click=ContextMenuState.delete, + ), + ), + rx.spacer(), + rx.alert_dialog.cancel(rx.button("Cancel")), + ), + ), + open=ContextMenuState.which_dialog_open == "delete", + on_open_change=ContextMenuState.set_which_dialog_open(""), + ) + + +def settings_dialog(): + return rx.dialog.root( + rx.dialog.content( + rx.dialog.title("Settings"), + rx.dialog.description( + rx.text("Set your settings in this settings dialog."), + margin_bottom="20px", + ), + rx.dialog.close( + rx.button("Close", on_click=ContextMenuState.save_settings), + ), + ), + open=ContextMenuState.which_dialog_open == "settings", + on_open_change=ContextMenuState.set_which_dialog_open(""), + ) + + +def context_menu_call_dialog() -> rx.Component: + return rx.vstack( + rx.context_menu.root( + rx.context_menu.trigger(rx.icon("ellipsis-vertical")), + rx.context_menu.content( + rx.context_menu.item( + "Delete", + on_click=ContextMenuState.set_which_dialog_open("delete"), + ), + rx.context_menu.item( + "Settings", + on_click=ContextMenuState.set_which_dialog_open("settings"), + ), + ), + ), + rx.cond( + ContextMenuState.which_dialog_open, + rx.heading(f"{ContextMenuState.which_dialog_open} dialog is open"), + ), + delete_dialog(), + settings_dialog(), + align="center", + ) +``` diff --git a/docs/library/overlay/dialog.md b/docs/library/overlay/dialog.md new file mode 100644 index 00000000000..604dc5c43c9 --- /dev/null +++ b/docs/library/overlay/dialog.md @@ -0,0 +1,283 @@ +--- +components: + - rx.dialog.root + - rx.dialog.trigger + - rx.dialog.title + - rx.dialog.content + - rx.dialog.description + - rx.dialog.close + +only_low_level: + - True + +DialogRoot: | + lambda **props: rx.dialog.root( + rx.dialog.trigger(rx.button("Open Dialog")), + rx.dialog.content( + rx.dialog.title("Welcome to Reflex!"), + rx.dialog.description( + "This is a dialog component. You can render anything you want in here.", + ), + rx.dialog.close( + rx.button("Close Dialog"), + ), + ), + **props, + ) + +DialogContent: | + lambda **props: rx.dialog.root( + rx.dialog.trigger(rx.button("Open Dialog")), + rx.dialog.content( + rx.dialog.title("Welcome to Reflex!"), + rx.dialog.description( + "This is a dialog component. You can render anything you want in here.", + ), + rx.dialog.close( + rx.button("Close Dialog"), + ), + **props, + ), + ) +--- + +```python exec +import reflex as rx +from pcweb.pages.docs import library +``` + +# Dialog + +The `dialog.root` contains all the parts of a dialog. + +The `dialog.trigger` wraps the control that will open the dialog. + +The `dialog.content` contains the content of the dialog. + +The `dialog.title` is a title that is announced when the dialog is opened. + +The `dialog.description` is a description that is announced when the dialog is opened. + +The `dialog.close` wraps the control that will close the dialog. + +```python demo +rx.dialog.root( + rx.dialog.trigger(rx.button("Open Dialog")), + rx.dialog.content( + rx.dialog.title("Welcome to Reflex!"), + rx.dialog.description( + "This is a dialog component. You can render anything you want in here.", + ), + rx.dialog.close( + rx.button("Close Dialog", size="3"), + ), + ), +) +``` + +## In context examples + +```python demo +rx.dialog.root( + rx.dialog.trigger( + rx.button("Edit Profile", size="4") + ), + rx.dialog.content( + rx.dialog.title("Edit Profile"), + rx.dialog.description( + "Change your profile details and preferences.", + size="2", + margin_bottom="16px", + ), + rx.flex( + rx.text("Name", as_="div", size="2", margin_bottom="4px", weight="bold"), + rx.input(default_value="Freja Johnson", placeholder="Enter your name"), + rx.text("Email", as_="div", size="2", margin_bottom="4px", weight="bold"), + rx.input(default_value="freja@example.com", placeholder="Enter your email"), + direction="column", + spacing="3", + ), + rx.flex( + rx.dialog.close( + rx.button("Cancel", color_scheme="gray", variant="soft"), + ), + rx.dialog.close( + rx.button("Save"), + ), + spacing="3", + margin_top="16px", + justify="end", + ), + ), +) +``` + +```python demo +rx.dialog.root( + rx.dialog.trigger(rx.button("View users", size="4")), + rx.dialog.content( + rx.dialog.title("Users"), + rx.dialog.description("The following users have access to this project."), + + rx.inset( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + ), + ), + ), + side="x", + margin_top="24px", + margin_bottom="24px", + ), + rx.flex( + rx.dialog.close( + rx.button("Close", variant="soft", color_scheme="gray"), + ), + spacing="3", + justify="end", + ), + ), +) +``` + +## Events when the Dialog opens or closes + +The `on_open_change` event is called when the `open` state of the dialog changes. It is used in conjunction with the `open` prop, which is passed to the event handler. + +```python demo exec +class DialogState(rx.State): + num_opens: int = 0 + opened: bool = False + + @rx.event + def count_opens(self, value: bool): + self.opened = value + self.num_opens += 1 + + +def dialog_example(): + return rx.flex( + rx.heading(f"Number of times dialog opened or closed: {DialogState.num_opens}"), + rx.heading(f"Dialog open: {DialogState.opened}"), + rx.dialog.root( + rx.dialog.trigger(rx.button("Open Dialog")), + rx.dialog.content( + rx.dialog.title("Welcome to Reflex!"), + rx.dialog.description( + "This is a dialog component. You can render anything you want in here.", + ), + rx.dialog.close( + rx.button("Close Dialog", size="3"), + ), + ), + on_open_change=DialogState.count_opens, + ), + direction="column", + spacing="3", + ) +``` + +Check out the [menu docs]({library.overlay.dropdown_menu.path}) for an example of opening a dialog from within a dropdown menu. + +## Form Submission to a Database from a Dialog + +This example adds new users to a database from a dialog using a form. + +1. It defines a User model with name and email fields. +2. The `add_user_to_db` method adds a new user to the database, checking for existing emails. +3. On form submission, it calls the `add_user_to_db` method. +4. The UI component has: + +- A button to open a dialog +- A dialog containing a form to add a new user +- Input fields for name and email +- Submit and Cancel buttons + +```python demo exec +class User(rx.Model, table=True): + """The user model.""" + name: str + email: str + +class State(rx.State): + + current_user: User = User() + + @rx.event + def add_user_to_db(self, form_data: dict): + self.current_user = form_data + ### Uncomment the code below to add your data to a database ### + # with rx.session() as session: + # if session.exec( + # select(User).where(user.email == self.current_user["email"]) + # ).first(): + # return rx.window_alert("User with this email already exists") + # session.add(User(**self.current_user)) + # session.commit() + + return rx.toast.info(f"User {self.current_user['name']} has been added.", position="bottom-right") + + +def index() -> rx.Component: + return rx.dialog.root( + rx.dialog.trigger( + rx.button( + rx.icon("plus", size=26), + rx.text("Add User", size="4"), + ), + ), + rx.dialog.content( + rx.dialog.title( + "Add New User", + ), + rx.dialog.description( + "Fill the form with the user's info", + ), + rx.form( + rx.flex( + rx.input( + placeholder="User Name", name="name" + ), + rx.input( + placeholder="user@reflex.dev", name="email" + ), + rx.flex( + rx.dialog.close( + rx.button( + "Cancel", + variant="soft", + color_scheme="gray", + ), + ), + rx.dialog.close( + rx.button("Submit", type="submit"), + ), + spacing="3", + justify="end", + ), + direction="column", + spacing="4", + ), + on_submit=State.add_user_to_db, + reset_on_submit=False, + ), + max_width="450px", + ), + ) +``` diff --git a/docs/library/overlay/drawer.md b/docs/library/overlay/drawer.md new file mode 100644 index 00000000000..a8beb954514 --- /dev/null +++ b/docs/library/overlay/drawer.md @@ -0,0 +1,119 @@ +--- +components: + - rx.drawer.root + - rx.drawer.trigger + - rx.drawer.overlay + - rx.drawer.portal + - rx.drawer.content + - rx.drawer.close + +only_low_level: + - True + +DrawerRoot: | + lambda **props: rx.drawer.root( + rx.drawer.trigger(rx.button("Open Drawer")), + rx.drawer.overlay(z_index="5"), + rx.drawer.portal( + rx.drawer.content( + rx.flex( + rx.drawer.close(rx.button("Close")), + ), + height="100%", + width="20em", + background_color="#FFF" + ), + ), + **props, + ) +--- + +```python exec +import reflex as rx +``` + +# Drawer + +```python demo +rx.drawer.root( + rx.drawer.trigger( + rx.button("Open Drawer") + ), + rx.drawer.overlay( + z_index="5" + ), + rx.drawer.portal( + rx.drawer.content( + rx.flex( + rx.drawer.close(rx.box(rx.button("Close"))), + align_items="start", + direction="column", + ), + top="auto", + right="auto", + height="100%", + width="20em", + padding="2em", + background_color="#FFF" + #background_color=rx.color("green", 3) + ) + ), + direction="left", +) +``` + +## Sidebar Menu with a Drawer and State + +This example shows how to create a sidebar menu with a drawer. The drawer is opened by clicking a button. The drawer contains links to different sections of the page. When a link is clicked the drawer closes and the page scrolls to the section. + +The `rx.drawer.root` component has an `open` prop that is set by the state variable `is_open`. Setting the `modal` prop to `False` allows the user to interact with the rest of the page while the drawer is open and allows the page to be scrolled when a user clicks one of the links. + +```python demo exec +class DrawerState(rx.State): + is_open: bool = False + + @rx.event + def toggle_drawer(self): + self.is_open = not self.is_open + +def drawer_content(): + return rx.drawer.content( + rx.flex( + rx.drawer.close(rx.button("Close", on_click=DrawerState.toggle_drawer)), + rx.link("Link 1", href="#test1", on_click=DrawerState.toggle_drawer), + rx.link("Link 2", href="#test2", on_click=DrawerState.toggle_drawer), + align_items="start", + direction="column", + ), + height="100%", + width="20%", + padding="2em", + background_color=rx.color("grass", 7), + ) + + +def lateral_menu(): + return rx.drawer.root( + rx.drawer.trigger(rx.button("Open Drawer", on_click=DrawerState.toggle_drawer)), + rx.drawer.overlay(), + rx.drawer.portal(drawer_content()), + open=DrawerState.is_open, + direction="left", + modal=False, + ) + +def drawer_sidebar(): + return rx.vstack( + lateral_menu(), + rx.section( + rx.heading("Test1", size="8"), + id='test1', + height="400px", + ), + rx.section( + rx.heading("Test2", size="8"), + id='test2', + height="400px", + ) + ) +``` diff --git a/docs/library/overlay/dropdown_menu.md b/docs/library/overlay/dropdown_menu.md new file mode 100644 index 00000000000..1db023e87cf --- /dev/null +++ b/docs/library/overlay/dropdown_menu.md @@ -0,0 +1,315 @@ +--- +components: + - rx.dropdown_menu.root + - rx.dropdown_menu.content + - rx.dropdown_menu.trigger + - rx.dropdown_menu.item + - rx.dropdown_menu.separator + - rx.dropdown_menu.sub_content + +only_low_level: + - True + +DropdownMenuRoot: | + lambda **props: rx.menu.root( + rx.menu.trigger(rx.button("drop down menu")), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Share"), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.menu.sub( + rx.menu.sub_trigger("More"), + rx.menu.sub_content( + rx.menu.item("Eradicate"), + rx.menu.item("Duplicate"), + rx.menu.item("Archive"), + ), + ), + ), + **props + ) + +DropdownMenuContent: | + lambda **props: rx.menu.root( + rx.menu.trigger(rx.button("drop down menu")), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Share"), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.menu.sub( + rx.menu.sub_trigger("More"), + rx.menu.sub_content( + rx.menu.item("Eradicate"), + rx.menu.item("Duplicate"), + rx.menu.item("Archive"), + ), + ), + **props, + ), + ) + +DropdownMenuItem: | + lambda **props: rx.menu.root( + rx.menu.trigger(rx.button("drop down menu")), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E", **props), + rx.menu.item("Share", **props), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red", **props), + rx.menu.sub( + rx.menu.sub_trigger("More"), + rx.menu.sub_content( + rx.menu.item("Eradicate", **props), + rx.menu.item("Duplicate", **props), + rx.menu.item("Archive", **props), + ), + ), + ), + ) + +DropdownMenuSub: | + lambda **props: rx.menu.root( + rx.menu.trigger(rx.button("drop down menu")), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Share"), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.menu.sub( + rx.menu.sub_trigger("More"), + rx.menu.sub_content( + rx.menu.item("Eradicate"), + rx.menu.item("Duplicate"), + rx.menu.item("Archive"), + ), + **props, + ), + ), + ) + +DropdownMenuSubTrigger: | + lambda **props: rx.menu.root( + rx.menu.trigger(rx.button("drop down menu")), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Share"), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.menu.sub( + rx.menu.sub_trigger("More", **props), + rx.menu.sub_content( + rx.menu.item("Eradicate"), + rx.menu.item("Duplicate"), + rx.menu.item("Archive"), + ), + ), + ), + ) + +DropdownMenuSubContent: | + lambda **props: rx.menu.root( + rx.menu.trigger(rx.button("drop down menu")), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Share"), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + rx.menu.sub( + rx.menu.sub_trigger("More"), + rx.menu.sub_content( + rx.menu.item("Eradicate"), + rx.menu.item("Duplicate"), + rx.menu.item("Archive"), + **props, + ), + ), + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Dropdown Menu + +A Dropdown Menu is a menu that offers a list of options that a user can select from. They are typically positioned near a button that will control their appearance and disappearance. + +A Dropdown Menu is composed of a `menu.root`, a `menu.trigger` and a `menu.content`. The `menu.trigger` is the element that the user interacts with to open the menu. It wraps the element that will open the dropdown menu. The `menu.content` is the component that pops out when the dropdown menu is open. + +The `menu.item` contains the actual dropdown menu items and sits under the `menu.content`. The `shortcut` prop is an optional shortcut command displayed next to the item text. + +The `menu.sub` contains all the parts of a submenu. There is a `menu.sub_trigger`, which is an item that opens a submenu. It must be rendered inside a `menu.sub` component. The `menu.sub_component` is the component that pops out when a submenu is open. It must also be rendered inside a `menu.sub` component. + +The `menu.separator` is used to visually separate items in a dropdown menu. + +```python demo +rx.menu.root( + rx.menu.trigger( + rx.button("Options", variant="soft"), + ), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Duplicate", shortcut="⌘ D"), + rx.menu.separator(), + rx.menu.item("Archive", shortcut="⌘ N"), + rx.menu.sub( + rx.menu.sub_trigger("More"), + rx.menu.sub_content( + rx.menu.item("Move to project…"), + rx.menu.item("Move to folder…"), + rx.menu.separator(), + rx.menu.item("Advanced options…"), + ), + ), + rx.menu.separator(), + rx.menu.item("Share"), + rx.menu.item("Add to favorites"), + rx.menu.separator(), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + ), +) +``` + +## Events when the Dropdown Menu opens or closes + +The `on_open_change` event, from the `menu.root`, is called when the `open` state of the dropdown menu changes. It is used in conjunction with the `open` prop, which is passed to the event handler. + +```python demo exec +class DropdownMenuState(rx.State): + num_opens: int = 0 + opened: bool = False + + @rx.event + def count_opens(self, value: bool): + self.opened = value + self.num_opens += 1 + + +def dropdown_menu_example(): + return rx.flex( + rx.heading(f"Number of times Dropdown Menu opened or closed: {DropdownMenuState.num_opens}"), + rx.heading(f"Dropdown Menu open: {DropdownMenuState.opened}"), + rx.menu.root( + rx.menu.trigger( + rx.button("Options", variant="soft", size="2"), + ), + rx.menu.content( + rx.menu.item("Edit", shortcut="⌘ E"), + rx.menu.item("Duplicate", shortcut="⌘ D"), + rx.menu.separator(), + rx.menu.item("Archive", shortcut="⌘ N"), + rx.menu.separator(), + rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"), + ), + on_open_change=DropdownMenuState.count_opens, + ), + direction="column", + spacing="3", + ) +``` + +## Opening a Dialog from Menu using State + +Accessing an overlay component from within another overlay component is a common use case but does not always work exactly as expected. + +The code below will not work as expected as because the dialog is within the menu and the dialog will only be open when the menu is open, rendering the dialog unusable. + +```python +rx.menu.root( + rx.menu.trigger(rx.icon("ellipsis-vertical")), + rx.menu.content( + rx.menu.item( + rx.dialog.root( + rx.dialog.trigger(rx.text("Edit")), + rx.dialog.content(....), + ..... + ), + ), + ), +) +``` + +In this example, we will show how to open a dialog box from a dropdown menu, where the menu will close and the dialog will open and be functional. + +```python demo exec +class DropdownMenuState2(rx.State): + which_dialog_open: str = "" + + @rx.event + def set_which_dialog_open(self, value: str): + self.which_dialog_open = value + + @rx.event + def delete(self): + yield rx.toast("Deleted item") + + @rx.event + def save_settings(self): + yield rx.toast("Saved settings") + + +def delete_dialog(): + return rx.alert_dialog.root( + rx.alert_dialog.content( + rx.alert_dialog.title("Are you Sure?"), + rx.alert_dialog.description( + rx.text( + "This action cannot be undone. Are you sure you want to delete this item?", + ), + margin_bottom="20px", + ), + rx.hstack( + rx.alert_dialog.action( + rx.button( + "Delete", + color_scheme="red", + on_click=DropdownMenuState2.delete, + ), + ), + rx.spacer(), + rx.alert_dialog.cancel(rx.button("Cancel")), + ), + ), + open=DropdownMenuState2.which_dialog_open == "delete", + on_open_change=DropdownMenuState2.set_which_dialog_open(""), + ) + + +def settings_dialog(): + return rx.dialog.root( + rx.dialog.content( + rx.dialog.title("Settings"), + rx.dialog.description( + rx.text("Set your settings in this settings dialog."), + margin_bottom="20px", + ), + rx.dialog.close( + rx.button("Close", on_click=DropdownMenuState2.save_settings), + ), + ), + open=DropdownMenuState2.which_dialog_open == "settings", + on_open_change=DropdownMenuState2.set_which_dialog_open(""), + ) + + +def menu_call_dialog() -> rx.Component: + return rx.vstack( + rx.menu.root( + rx.menu.trigger(rx.icon("menu")), + rx.menu.content( + rx.menu.item( + "Delete", + on_click=DropdownMenuState2.set_which_dialog_open("delete"), + ), + rx.menu.item( + "Settings", + on_click=DropdownMenuState2.set_which_dialog_open("settings"), + ), + ), + ), + rx.cond( + DropdownMenuState2.which_dialog_open, + rx.heading(f"{DropdownMenuState2.which_dialog_open} dialog is open"), + ), + delete_dialog(), + settings_dialog(), + align="center", + ) +``` diff --git a/docs/library/overlay/hover_card.md b/docs/library/overlay/hover_card.md new file mode 100644 index 00000000000..6a23341ca74 --- /dev/null +++ b/docs/library/overlay/hover_card.md @@ -0,0 +1,126 @@ +--- +components: + - rx.hover_card.root + - rx.hover_card.content + - rx.hover_card.trigger + +only_low_level: + - True + +HoverCardRoot: | + lambda **props: rx.hover_card.root( + rx.hover_card.trigger( + rx.link("Hover over me"), + ), + rx.hover_card.content( + rx.text("This is the tooltip content."), + ), + **props + ) + +HoverCardContent: | + lambda **props: rx.hover_card.root( + rx.hover_card.trigger( + rx.link("Hover over me"), + ), + rx.hover_card.content( + rx.text("This is the tooltip content."), + **props + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Hovercard + +The `hover_card.root` contains all the parts of a hover card. + +The `hover_card.trigger` wraps the link that will open the hover card. + +The `hover_card.content` contains the content of the open hover card. + +```python demo +rx.text( + "Hover over the text to see the tooltip. ", + rx.hover_card.root( + rx.hover_card.trigger( + rx.link("Hover over me", color_scheme="blue", underline="always"), + ), + rx.hover_card.content( + rx.text("This is the hovercard content."), + ), + ), +) +``` + +```python demo +rx.text( + "Hover over the text to see the tooltip. ", + rx.hover_card.root( + rx.hover_card.trigger( + rx.link("Hover over me", color_scheme="blue", underline="always"), + ), + rx.hover_card.content( + rx.grid( + rx.inset( + side="left", + pr="current", + background="url('https://images.unsplash.com/5/unsplash-kitsune-4.jpg') center/cover", + height="full", + ), + rx.box( + rx.text_area(placeholder="Write a comment…", style={"height": 80}), + rx.flex( + rx.checkbox("Send to group"), + spacing="3", + margin_top="12px", + justify="between", + ), + padding_left="12px", + ), + columns="120px 1fr", + ), + style={"width": 360}, + ), + ), +) +``` + +## Events when the Hovercard opens or closes + +The `on_open_change` event is called when the `open` state of the hovercard changes. It is used in conjunction with the `open` prop, which is passed to the event handler. + +```python demo exec +class HovercardState(rx.State): + num_opens: int = 0 + opened: bool = False + + @rx.event + def count_opens(self, value: bool): + self.opened = value + self.num_opens += 1 + + +def hovercard_example(): + return rx.flex( + rx.heading(f"Number of times hovercard opened or closed: {HovercardState.num_opens}"), + rx.heading(f"Hovercard open: {HovercardState.opened}"), + rx.text( + "Hover over the text to see the hover card. ", + rx.hover_card.root( + rx.hover_card.trigger( + rx.link("Hover over me", color_scheme="blue", underline="always"), + ), + rx.hover_card.content( + rx.text("This is the tooltip content."), + ), + on_open_change=HovercardState.count_opens, + ), + ), + direction="column", + spacing="3", + ) +``` diff --git a/docs/library/overlay/popover.md b/docs/library/overlay/popover.md new file mode 100644 index 00000000000..2d6b81f4728 --- /dev/null +++ b/docs/library/overlay/popover.md @@ -0,0 +1,224 @@ +--- +components: + - rx.popover.root + - rx.popover.content + - rx.popover.trigger + - rx.popover.close + +only_low_level: + - True + +PopoverRoot: | + lambda **props: rx.popover.root( + rx.popover.trigger( + rx.button("Popover"), + ), + rx.popover.content( + rx.flex( + rx.text("Simple Example"), + rx.popover.close( + rx.button("Close"), + ), + direction="column", + spacing="3", + ), + ), + **props + ) + +PopoverContent: | + lambda **props: rx.popover.root( + rx.popover.trigger( + rx.button("Popover"), + ), + rx.popover.content( + rx.flex( + rx.text("Simple Example"), + rx.popover.close( + rx.button("Close"), + ), + direction="column", + spacing="3", + ), + **props + ), + ) +--- + +```python exec +import reflex as rx +``` + +# Popover + +A popover displays content, triggered by a button. + +The `popover.root` contains all the parts of a popover. + +The `popover.trigger` contains the button that toggles the popover. + +The `popover.content` is the component that pops out when the popover is open. + +The `popover.close` is the button that closes an open popover. + +## Basic Example + +```python demo +rx.popover.root( + rx.popover.trigger( + rx.button("Popover"), + ), + rx.popover.content( + rx.flex( + rx.text("Simple Example"), + rx.popover.close( + rx.button("Close"), + ), + direction="column", + spacing="3", + ), + ), +) +``` + +## Examples in Context + +```python demo + +rx.popover.root( + rx.popover.trigger( + rx.button("Comment", variant="soft"), + ), + rx.popover.content( + rx.flex( + rx.avatar( + "2", + fallback="RX", + radius="full" + ), + rx.box( + rx.text_area(placeholder="Write a comment…", style={"height": 80}), + rx.flex( + rx.checkbox("Send to group"), + rx.popover.close( + rx.button("Comment", size="1") + ), + spacing="3", + margin_top="12px", + justify="between", + ), + flex_grow="1", + ), + spacing="3" + ), + style={"width": 360}, + ) +) +``` + +```python demo +rx.popover.root( + rx.popover.trigger( + rx.button("Feedback", variant="classic"), + ), + rx.popover.content( + rx.inset( + side="top", + background="url('https://images.unsplash.com/5/unsplash-kitsune-4.jpg') center/cover", + height="100px", + ), + rx.box( + rx.text_area(placeholder="Write a comment…", style={"height": 80}), + rx.flex( + rx.checkbox("Send to group"), + rx.popover.close( + rx.button("Comment", size="1") + ), + spacing="3", + margin_top="12px", + justify="between", + ), + padding_top="12px", + ), + style={"width": 360}, + ) +) +``` + +## Popover with dynamic title + +Code like below will not work as expected and it is necessary to place the dynamic title (`Index2State.language`) inside of an `rx.text` component. + +```python +class Index2State(rx.State): + language: str = "EN" + +def index() -> rx.Component: + return rx.popover.root( + rx.popover.trigger( + rx.button(Index2State.language), + ), + rx.popover.content( + rx.text('Success') + ) + ) +``` + +This code will work: + +```python demo exec +class Index2State(rx.State): + language: str = "EN" + +def index() -> rx.Component: + return rx.popover.root( + rx.popover.trigger( + rx.button( + rx.text(Index2State.language) + ), + ), + rx.popover.content( + rx.text('Success') + ) + ) +``` + +## Events when the Popover opens or closes + +The `on_open_change` event is called when the `open` state of the popover changes. It is used in conjunction with the `open` prop, which is passed to the event handler. + +```python demo exec +class PopoverState(rx.State): + num_opens: int = 0 + opened: bool = False + + @rx.event + def count_opens(self, value: bool): + self.opened = value + self.num_opens += 1 + + +def popover_example(): + return rx.flex( + rx.heading(f"Number of times popover opened or closed: {PopoverState.num_opens}"), + rx.heading(f"Popover open: {PopoverState.opened}"), + rx.popover.root( + rx.popover.trigger( + rx.button("Popover"), + ), + rx.popover.content( + rx.flex( + rx.text("Simple Example"), + rx.popover.close( + rx.button("Close"), + ), + direction="column", + spacing="3", + ), + ), + on_open_change=PopoverState.count_opens, + ), + direction="column", + spacing="3", + ) +``` diff --git a/docs/library/overlay/toast.md b/docs/library/overlay/toast.md new file mode 100644 index 00000000000..7c5c666060c --- /dev/null +++ b/docs/library/overlay/toast.md @@ -0,0 +1,105 @@ +--- +components: + - rx.toast.provider +--- + +```python exec +import reflex as rx +``` + +# Toast + +A `rx.toast` is a non-blocking notification that disappears after a certain amount of time. It is often used to show a message to the user without interrupting their workflow. + +## Usage + +You can use `rx.toast` as an event handler for any component that triggers an action. + +```python demo +rx.button("Show Toast", on_click=rx.toast("Hello, World!")) +``` + +### Usage in State + +You can also use `rx.toast` in a state to show a toast when a specific action is triggered, using `yield`. + +```python demo exec +import asyncio +class ToastState(rx.State): + + @rx.event + async def fetch_data(self): + # Simulate fetching data for a 2-second delay + await asyncio.sleep(2) + # Shows a toast when the data is fetched + yield rx.toast("Data fetched!") + + +def render(): + return rx.button("Get Data", on_click=ToastState.fetch_data) +``` + +## Interaction + +If you want to interact with a toast, a few props are available to customize the behavior. + +By passing a `ToastAction` to the `action` or `cancel` prop, you can trigger an action when the toast is clicked or when it is closed. + +```python demo +rx.button("Show Toast", on_click=rx.toast("Hello, World!", duration=5000, close_button=True)) +``` + +### Presets + +`rx.toast` has some presets that you can use to show different types of toasts. + +```python demo +rx.hstack( + rx.button("Success", on_click=rx.toast.success("Success!"), color_scheme="green"), + rx.button("Error", on_click=rx.toast.error("Error!"), color_scheme="red"), + rx.button("Warning", on_click=rx.toast.warning("Warning!"), color_scheme="orange"), + rx.button("Info", on_click=rx.toast.info("Info!"), color_scheme="blue"), +) +``` + +### Customization + +If the presets don't fit your needs, you can customize the toasts by passing to `rx.toast` or to `rx.toast.options` some kwargs. + +```python demo +rx.button( + "Custom", + on_click=rx.toast( + "Custom Toast!", + position="top-right", + style={"background-color": "green", "color": "white", "border": "1px solid green", "border-radius": "0.53m"} + ) +) +``` + +The following props are available for customization: + +- `description`: `str | Var`: Toast's description, renders underneath the title. +- `close_button`: `bool`: Whether to show the close button. +- `invert`: `bool`: Dark toast in light mode and vice versa. +- `important`: `bool`: Control the sensitivity of the toast for screen readers. +- `duration`: `int`: Time in milliseconds that should elapse before automatically closing the toast. +- `position`: `LiteralPosition`: Position of the toast. +- `dismissible`: `bool`: If false, it'll prevent the user from dismissing the toast. +- `action`: `ToastAction`: Renders a primary button, clicking it will close the toast. +- `cancel`: `ToastAction`: Renders a secondary button, clicking it will close the toast. +- `id`: `str | Var`: Custom id for the toast. +- `unstyled`: `bool`: Removes the default styling, which allows for easier customization. +- `style`: `Style`: Custom style for the toast. +- `on_dismiss`: `Any`: The function gets called when either the close button is clicked, or the toast is swiped. +- `on_auto_close`: `Any`: Function that gets called when the toast disappears automatically after it's timeout (`duration` prop). + +## Toast Provider + +Using the `rx.toast` function require to have a toast provider in your app. + +`rx.toast.provider` is a component that provides a context for displaying toasts. It should be placed at the root of your app. + +```md alert warning +# In most case you will not need to include this component directly, as it is already included in `rx.app` as the `overlay_component` for displaying connections errors. +``` diff --git a/docs/library/overlay/tooltip.md b/docs/library/overlay/tooltip.md new file mode 100644 index 00000000000..32b9993e6cd --- /dev/null +++ b/docs/library/overlay/tooltip.md @@ -0,0 +1,60 @@ +--- +components: + - rx.tooltip + +Tooltip: | + lambda **props: rx.tooltip( + rx.button("Hover over me"), + content="This is the tooltip content.", + **props, + ) +--- + +```python exec +import reflex as rx +``` + +# Tooltip + +A `tooltip` displays informative information when users hover over or focus on an element. + +It takes a `content` prop, which is the content associated with the tooltip. + +```python demo +rx.tooltip( + rx.button("Hover over me"), + content="This is the tooltip content.", +) +``` + +## Events when the Tooltip opens or closes + +The `on_open_change` event is called when the `open` state of the tooltip changes. It is used in conjunction with the `open` prop, which is passed to the event handler. + +```python demo exec +class TooltipState(rx.State): + num_opens: int = 0 + opened: bool = False + + @rx.event + def count_opens(self, value: bool): + self.opened = value + self.num_opens += 1 + + +def index(): + return rx.flex( + rx.heading(f"Number of times tooltip opened or closed: {TooltipState.num_opens}"), + rx.heading(f"Tooltip open: {TooltipState.opened}"), + rx.text( + "Hover over the button to see the tooltip.", + rx.tooltip( + rx.button("Hover over me"), + content="This is the tooltip content.", + on_open_change=TooltipState.count_opens, + ), + ), + direction="column", + spacing="3", + ) +``` diff --git a/docs/library/tables-and-data-grids/data_editor.md b/docs/library/tables-and-data-grids/data_editor.md new file mode 100644 index 00000000000..c64eac81b1b --- /dev/null +++ b/docs/library/tables-and-data-grids/data_editor.md @@ -0,0 +1,356 @@ +--- +components: + - rx.data_editor +--- + +# Data Editor + +A datagrid editor based on [Glide Data Grid](https://grid.glideapps.com/) + +```python exec +import reflex as rx +from pcweb.pages.docs import library +from typing import Any + +columns: list[dict[str, str]] = [ + { + "title":"Code", + "type": "str", + }, + { + "title":"Value", + "type": "int", + }, + { + "title":"Activated", + "type": "bool", + }, +] +data: list[list[Any]] = [ + ["A", 1, True], + ["B", 2, False], + ["C", 3, False], + ["D", 4, True], + ["E", 5, True], + ["F", 6, False], +] + +``` + +This component is introduced as an alternative to the [datatable]({library.tables_and_data_grids.data_table.path}) to support editing the displayed data. + +## Columns + +The columns definition should be a `list` of `dict`, each `dict` describing the associated columns. +Property of a column dict: + +- `title`: The text to display in the header of the column. +- `id`: An id for the column, if not defined, will default to a lower case of `title` +- `width`: The width of the column. +- `type`: The type of the columns, default to `"str"`. + +## Data + +The `data` props of `rx.data_editor` accept a `list` of `list`, where each `list` represent a row of data to display in the table. + +## Simple Example + +Here is a basic example of using the data_editor representing data with no interaction and no styling. Below we define the `columns` and the `data` which are taken in by the `rx.data_editor` component. When we define the `columns` we must define a `title` and a `type` for each column we create. The columns in the `data` must then match the defined `type` or errors will be thrown. + +```python demo box +rx.data_editor( + columns=columns, + data=data, +) +``` + +```python +columns: list[dict[str, str]] = [ + { + "title":"Code", + "type": "str", + }, + { + "title":"Value", + "type": "int", + }, + { + "title":"Activated", + "type": "bool", + }, +] +data: list[list[Any]] = [ + ["A", 1, True], + ["B", 2, False], + ["C", 3, False], + ["D", 4, True], + ["E", 5, True], + ["F", 6, False], +] +``` + +```python +rx.data_editor( + columns=columns, + data=data, +) +``` + +## Interactive Example + +```python exec +class DataEditorState_HP(rx.State): + + clicked_data: str = "Cell clicked: " + cols: list[Any] = [ + {"title": "Title", "type": "str"}, + { + "title": "Name", + "type": "str", + "group": "Data", + "width": 300, + }, + { + "title": "Birth", + "type": "str", + "id": "date", + "group": "Data", + "width": 150, + }, + { + "title": "Human", + "type": "bool", + "group": "Data", + "width": 80, + }, + { + "title": "House", + "type": "str", + "id": "date", + "group": "Data", + }, + { + "title": "Wand", + "type": "str", + "id": "date", + "group": "Data", + "width": 250, + }, + { + "title": "Patronus", + "type": "str", + "id": "date", + "group": "Data", + }, + { + "title": "Blood status", + "type": "str", + "id": "date", + "group": "Data", + "width": 200, + } + ] + + data = [ + ["1", "Harry James Potter", "31 July 1980", True, "Gryffindor", "11' Holly phoenix feather", "Stag", "Half-blood"], + ["2", "Ronald Bilius Weasley", "1 March 1980", True,"Gryffindor", "12' Ash unicorn tail hair", "Jack Russell terrier", "Pure-blood"], + ["3", "Hermione Jean Granger", "19 September, 1979", True, "Gryffindor", "10¾' vine wood dragon heartstring", "Otter", "Muggle-born"], + ["4", "Albus Percival Wulfric Brian Dumbledore", "Late August 1881", True, "Gryffindor", "15' Elder Thestral tail hair core", "Phoenix", "Half-blood"], + ["5", "Rubeus Hagrid", "6 December 1928", False, "Gryffindor", "16' Oak unknown core", "None", "Part-Human (Half-giant)"], + ["6", "Fred Weasley", "1 April, 1978", True, "Gryffindor", "Unknown", "Unknown", "Pure-blood"], + ] + + def click_cell(self, pos): + col, row = pos + yield self.get_clicked_data(pos) + + + def get_clicked_data(self, pos) -> str: + self.clicked_data = f"Cell clicked: {pos}" + +``` + +Here we define a State, as shown below, that allows us to print the location of the cell as a heading when we click on it, using the `on_cell_clicked` `event trigger`. Check out all the other `event triggers` that you can use with datatable at the bottom of this page. We also define a `group` with a label `Data`. This groups all the columns with this `group` label under a larger group `Data` as seen in the table below. + +```python demo box +rx.heading(DataEditorState_HP.clicked_data) +``` + +```python demo box +rx.data_editor( + columns=DataEditorState_HP.cols, + data=DataEditorState_HP.data, + on_cell_clicked=DataEditorState_HP.click_cell, +) +``` + +```python +class DataEditorState_HP(rx.State): + + clicked_data: str = "Cell clicked: " + + cols: list[Any] = [ + { + "title": "Title", + "type": "str" + }, + { + "title": "Name", + "type": "str", + "group": "Data", + "width": 300, + }, + { + "title": "Birth", + "type": "str", + "group": "Data", + "width": 150, + }, + { + "title": "Human", + "type": "bool", + "group": "Data", + "width": 80, + }, + { + "title": "House", + "type": "str", + "group": "Data", + }, + { + "title": "Wand", + "type": "str", + "group": "Data", + "width": 250, + }, + { + "title": "Patronus", + "type": "str", + "group": "Data", + }, + { + "title": "Blood status", + "type": "str", + "group": "Data", + "width": 200, + } + ] + + data = [ + ["1", "Harry James Potter", "31 July 1980", True, "Gryffindor", "11' Holly phoenix feather", "Stag", "Half-blood"], + ["2", "Ronald Bilius Weasley", "1 March 1980", True,"Gryffindor", "12' Ash unicorn tail hair", "Jack Russell terrier", "Pure-blood"], + ["3", "Hermione Jean Granger", "19 September, 1979", True, "Gryffindor", "10¾' vine wood dragon heartstring", "Otter", "Muggle-born"], + ["4", "Albus Percival Wulfric Brian Dumbledore", "Late August 1881", True, "Gryffindor", "15' Elder Thestral tail hair core", "Phoenix", "Half-blood"], + ["5", "Rubeus Hagrid", "6 December 1928", False, "Gryffindor", "16' Oak unknown core", "None", "Part-Human (Half-giant)"], + ["6", "Fred Weasley", "1 April, 1978", True, "Gryffindor", "Unknown", "Unknown", "Pure-blood"], + ] + + + def click_cell(self, pos): + col, row = pos + yield self.get_clicked_data(pos) + + + def get_clicked_data(self, pos) -> str: + self.clicked_data = f"Cell clicked: \{pos}" +``` + +```python +rx.data_editor( + columns=DataEditorState_HP.cols, + data=DataEditorState_HP.data, + on_cell_clicked=DataEditorState_HP.click_cell, +) +``` + +## Styling Example + +Now let's style our datatable to make it look more aesthetic and easier to use. We must first import `DataEditorTheme` and then we can start setting our style props as seen below in `dark_theme`. + +We then set these themes using `theme=DataEditorTheme(**dark_theme)`. On top of the styling we can also set some `props` to make some other aesthetic changes to our datatable. We have set the `row_height` to equal `50` so that the content is easier to read. We have also made the `smooth_scroll_x` and `smooth_scroll_y` equal `True` so that we can smoothly scroll along the columns and rows. Finally, we added `column_select=single`, where column select can take any of the following values `none`, `single` or `multiple`. + +```python exec +from reflex.components.datadisplay.dataeditor import DataEditorTheme +dark_theme = { + "accentColor": "#8c96ff", + "accentLight": "rgba(202, 206, 255, 0.253)", + "textDark": "#ffffff", + "textMedium": "#b8b8b8", + "textLight": "#a0a0a0", + "textBubble": "#ffffff", + "bgIconHeader": "#b8b8b8", + "fgIconHeader": "#000000", + "textHeader": "#a1a1a1", + "textHeaderSelected": "#000000", + "bgCell": "#16161b", + "bgCellMedium": "#202027", + "bgHeader": "#212121", + "bgHeaderHasFocus": "#474747", + "bgHeaderHovered": "#404040", + "bgBubble": "#212121", + "bgBubbleSelected": "#000000", + "bgSearchResult": "#423c24", + "borderColor": "rgba(225,225,225,0.2)", + "drilldownBorder": "rgba(225,225,225,0.4)", + "linkColor": "#4F5DFF", + "headerFontStyle": "bold 14px", + "baseFontStyle": "13px", + "fontFamily": "Inter, Roboto, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, noto, arial, sans-serif", +} +``` + +```python demo box +rx.data_editor( + columns=DataEditorState_HP.cols, + data=DataEditorState_HP.data, + row_height=80, + smooth_scroll_x=True, + smooth_scroll_y=True, + column_select="single", + theme=DataEditorTheme(**dark_theme), + height="30vh", +) +``` + +```python +from reflex.components.datadisplay.dataeditor import DataEditorTheme +dark_theme_snake_case = { + "accent_color": "#8c96ff", + "accent_light": "rgba(202, 206, 255, 0.253)", + "text_dark": "#ffffff", + "text_medium": "#b8b8b8", + "text_light": "#a0a0a0", + "text_bubble": "#ffffff", + "bg_icon_header": "#b8b8b8", + "fg_icon_header": "#000000", + "text_header": "#a1a1a1", + "text_header_selected": "#000000", + "bg_cell": "#16161b", + "bg_cell_medium": "#202027", + "bg_header": "#212121", + "bg_header_has_focus": "#474747", + "bg_header_hovered": "#404040", + "bg_bubble": "#212121", + "bg_bubble_selected": "#000000", + "bg_search_result": "#423c24", + "border_color": "rgba(225,225,225,0.2)", + "drilldown_border": "rgba(225,225,225,0.4)", + "link_color": "#4F5DFF", + "header_font_style": "bold 14px", + "base_font_style": "13px", + "font_family": "Inter, Roboto, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, noto, arial, sans-serif", +} +``` + +```python +rx.data_editor( + columns=DataEditorState_HP.cols, + data=DataEditorState_HP.data, + row_height=80, + smooth_scroll_x=True, + smooth_scroll_y=True, + column_select="single", + theme=DataEditorTheme(**dark_theme), + height="30vh", +) +``` diff --git a/docs/library/tables-and-data-grids/data_table.md b/docs/library/tables-and-data-grids/data_table.md new file mode 100644 index 00000000000..d8ef4c3f94c --- /dev/null +++ b/docs/library/tables-and-data-grids/data_table.md @@ -0,0 +1,72 @@ +--- +components: + - rx.data_table +--- + +```python exec +import reflex as rx +from pcweb.pages.docs import library +``` + +# Data Table + +The data table component is a great way to display static data in a table format. +You can pass in a pandas dataframe to the data prop to create the table. + +In this example we will read data from a csv file, convert it to a pandas dataframe and display it in a data_table. + +We will also add a search, pagination, sorting to the data_table to make it more accessible. + +If you want to [add, edit or remove data]({library.tables_and_data_grids.table.path}) in your app or deal with anything but static data then the [`rx.table`]({library.tables_and_data_grids.table.path}) might be a better fit for your use case. + +```python demo box +rx.data_table( + data=[ + ["Avery Bradley", "6-2", 25.0], + ["Jae Crowder", "6-6", 25.0], + ["John Holland", "6-5", 27.0], + ["R.J. Hunter", "6-5", 22.0], + ["Jonas Jerebko", "6-10", 29.0], + ["Amir Johnson", "6-9", 29.0], + ["Jordan Mickey", "6-8", 21.0], + ["Kelly Olynyk", "7-0", 25.0], + ["Terry Rozier", "6-2", 22.0], + ["Marcus Smart", "6-4", 22.0], + ], + columns=["Name", "Height", "Age"], + pagination=True, + search=True, + sort=True, +) +``` + +```python +import pandas as pd +nba_data = pd.read_csv("data/nba.csv") +... +rx.data_table( + data = nba_data[["Name", "Height", "Age"]], + pagination= True, + search= True, + sort= True, +) +``` + +📊 **Dataset source:** [nba.csv](https://media.geeksforgeeks.org/wp-content/uploads/nba.csv) + +The example below shows how to create a data table from from a list. + +```python +class State(rx.State): + data: List = [ + ["Lionel", "Messi", "PSG"], + ["Christiano", "Ronaldo", "Al-Nasir"] + ] + columns: List[str] = ["First Name", "Last Name"] + +def index(): + return rx.data_table( + data=State.data, + columns=State.columns, + ) +``` diff --git a/docs/library/tables-and-data-grids/table.md b/docs/library/tables-and-data-grids/table.md new file mode 100644 index 00000000000..140ae333674 --- /dev/null +++ b/docs/library/tables-and-data-grids/table.md @@ -0,0 +1,1207 @@ +--- +components: + - rx.table.root + - rx.table.header + - rx.table.row + - rx.table.column_header_cell + - rx.table.body + - rx.table.cell + - rx.table.row_header_cell + +only_low_level: + - True + +TableRoot: | + lambda **props: rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + ), + ), + width="80%", + **props, + ) + +TableRow: | + lambda **props: rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + **props, + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa"), + rx.table.cell(rx.text("danilo@example.com", as_="p"), rx.text("danilo@yahoo.com", as_="p"), rx.text("danilo@gmail.com", as_="p"),), + rx.table.cell("Developer"), + **props, + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + **props, + ), + ), + width="80%", + ) + +TableColumnHeaderCell: | + lambda **props: rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name", **props,), + rx.table.column_header_cell("Email", **props,), + rx.table.column_header_cell("Group", **props,), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + ), + ), + width="80%", + ) + +TableCell: | + lambda **props: rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa"), + rx.table.cell("danilo@example.com", **props,), + rx.table.cell("Developer", **props,), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com", **props,), + rx.table.cell("Admin", **props,), + ), + ), + width="80%", + ) + +TableRowHeaderCell: | + lambda **props: rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Rosa", **props,), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa", **props,), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + ), + ), + width="80%", + ) +--- + +```python exec +import reflex as rx +from pcweb.models import Customer +from pcweb.pages.docs import vars, events, database, library, components +``` + +# Table + +A semantic table for presenting tabular data. + +If you just want to [represent static data]({library.tables_and_data_grids.data_table.path}) then the [`rx.data_table`]({library.tables_and_data_grids.data_table.path}) might be a better fit for your use case as it comes with in-built pagination, search and sorting. + +## Basic Example + +```python demo +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell("Danilo Sousa"), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + ), + rx.table.row( + rx.table.row_header_cell("Zahra Ambessa"), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + ),rx.table.row( + rx.table.row_header_cell("Jasper Eriks"), + rx.table.cell("jasper@example.com"), + rx.table.cell("Developer"), + ), + ), + width="100%", +) +``` + +```md alert info +# Set the table `width` to fit within its container and prevent it from overflowing. +``` + +## Showing State data (using foreach) + +Many times there is a need for the data we represent in our table to be dynamic. Dynamic data must be in `State`. Later we will show an example of how to access data from a database and how to load data from a source file. + +In this example there is a `people` data structure in `State` that is [iterated through using `rx.foreach`]({components.rendering_iterables.path}). + +```python demo exec +class TableForEachState(rx.State): + people: list[list] = [ + ["Danilo Sousa", "danilo@example.com", "Developer"], + ["Zahra Ambessa", "zahra@example.com", "Admin"], + ["Jasper Eriks", "jasper@example.com", "Developer"], + ] + +def show_person(person: list): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(person[0]), + rx.table.cell(person[1]), + rx.table.cell(person[2]), + ) + +def foreach_table_example(): + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body(rx.foreach(TableForEachState.people, show_person)), + width="100%", + ) +``` + +It is also possible to define a `class` such as `Person` below and then iterate through this data structure, as a `list[Person]`. + +```python +import dataclasses + +@dataclasses.dataclass +class Person: + full_name: str + email: str + group: str +``` + +## Sorting and Filtering (Searching) + +In this example we show two approaches to sort and filter data: + +1. Using SQL-like operations for database-backed models (simulated) +2. Using Python operations for in-memory data + +Both approaches use the same UI components: `rx.select` for sorting and `rx.input` for filtering. + +### Approach 1: Database Filtering and Sorting + +For database-backed models, we typically use SQL queries with `select`, `where`, and `order_by`. In this example, we'll simulate this behavior with mock data. + +```python demo exec +# Simulating database operations with mock data +class DatabaseTableState(rx.State): + # Mock data to simulate database records + users: list = [ + {"name": "John Doe", "email": "john@example.com", "phone": "555-1234", "address": "123 Main St"}, + {"name": "Jane Smith", "email": "jane@example.com", "phone": "555-5678", "address": "456 Oak Ave"}, + {"name": "Bob Johnson", "email": "bob@example.com", "phone": "555-9012", "address": "789 Pine Rd"}, + {"name": "Alice Brown", "email": "alice@example.com", "phone": "555-3456", "address": "321 Maple Dr"}, + ] + filtered_users: list[dict] = [] + sort_value = "" + search_value = "" + + + @rx.event + def load_entries(self): + """Simulate querying the database with filter and sort.""" + # Start with all users + result = self.users.copy() + + # Apply filtering if search value exists + if self.search_value != "": + search_term = self.search_value.lower() + result = [ + user for user in result + if any(search_term in str(value).lower() for value in user.values()) + ] + + # Apply sorting if sort column is selected + if self.sort_value != "": + result = sorted(result, key=lambda x: x[self.sort_value]) + + self.filtered_users = result + yield + + @rx.event + def sort_values(self, sort_value): + """Update sort value and reload data.""" + self.sort_value = sort_value + yield DatabaseTableState.load_entries() + + @rx.event + def filter_values(self, search_value): + """Update search value and reload data.""" + self.search_value = search_value + yield DatabaseTableState.load_entries() + + +def show_customer(user): + """Show a customer in a table row.""" + return rx.table.row( + rx.table.cell(user["name"]), + rx.table.cell(user["email"]), + rx.table.cell(user["phone"]), + rx.table.cell(user["address"]), + ) + + +def database_table_example(): + return rx.vstack( + rx.select( + ["name", "email", "phone", "address"], + placeholder="Sort By: Name", + on_change=lambda value: DatabaseTableState.sort_values(value), + ), + rx.input( + placeholder="Search here...", + on_change=lambda value: DatabaseTableState.filter_values(value), + ), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState.filtered_users, show_customer)), + on_mount=DatabaseTableState.load_entries, + width="100%", + ), + width="100%", + ) +``` + +### Approach 2: In-Memory Filtering and Sorting + +For in-memory data, we use Python operations like `sorted()` and list comprehensions. + +The state variable `_people` is set to be a backend-only variable. This is done in case the variable is very large in order to reduce network traffic and improve performance. + +When a `select` item is selected, the `on_change` event trigger is hooked up to the `set_sort_value` event handler. Every base var has a built-in event handler to set its value for convenience, called `set_VARNAME`. + +`current_people` is an `rx.var(cache=True)`. It is a var that is only recomputed when the other state vars it depends on change. This ensures that the `People` shown in the table are always up to date whenever they are searched or sorted. + +```python demo exec +import dataclasses + +@dataclasses.dataclass +class Person: + full_name: str + email: str + group: str + + +class InMemoryTableState(rx.State): + + _people: list[Person] = [ + Person(full_name="Danilo Sousa", email="danilo@example.com", group="Developer"), + Person(full_name="Zahra Ambessa", email="zahra@example.com", group="Admin"), + Person(full_name="Jasper Eriks", email="zjasper@example.com", group="B-Developer"), + ] + + sort_value = "" + search_value = "" + + @rx.event + def set_sort_value(self, value: str): + self.sort_value = value + + @rx.event + def set_search_value(self, value: str): + self.search_value = value + + @rx.var(cache=True) + def current_people(self) -> list[Person]: + people = self._people + + if self.sort_value != "": + people = sorted( + people, key=lambda user: getattr(user, self.sort_value).lower() + ) + + if self.search_value != "": + people = [ + person for person in people + if any( + self.search_value.lower() in getattr(person, attr).lower() + for attr in ['full_name', 'email', 'group'] + ) + ] + return people + + +def show_person(person: Person): + """Show a person in a table row.""" + return rx.table.row( + rx.table.cell(person.full_name), + rx.table.cell(person.email), + rx.table.cell(person.group), + ) + +def in_memory_table_example(): + return rx.vstack( + rx.select( + ["full_name", "email", "group"], + placeholder="Sort By: full_name", + on_change=InMemoryTableState.set_sort_value, + ), + rx.input( + placeholder="Search here...", + on_change=InMemoryTableState.set_search_value, + ), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Full name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Group"), + ), + ), + rx.table.body(rx.foreach(InMemoryTableState.current_people, show_person)), + width="100%", + ), + width="100%", + ) +``` + +### When to Use Each Approach + +- **Database Approach**: Best for large datasets or when the data already exists in a database +- **In-Memory Approach**: Best for smaller datasets, prototyping, or when the data is static or loaded from an API + +Both approaches provide the same user experience with filtering and sorting functionality. + +# Database + +The more common use case for building an `rx.table` is to use data from a database. + +The code below shows how to load data from a database and place it in an `rx.table`. + +## Loading data into table + +A `Customer` [model]({database.tables.path}) is defined that inherits from `rx.Model`. + +The `load_entries` event handler executes a [query]({database.queries.path}) that is used to request information from a database table. This `load_entries` event handler is called on the `on_mount` event trigger of the `rx.table.root`. + +If you want to load the data when the page in the app loads you can set `on_load` in `app.add_page()` to equal this event handler, like `app.add_page(page_name, on_load=State.load_entries)`. + +```python +class Customer(rx.Model, table=True): + """The customer model.""" + name: str + email: str + phone: str + address: str +``` + +```python exec +class DatabaseTableState(rx.State): + + users: list[dict] = [] + + @rx.event + def load_entries(self): + """Get all users from the database.""" + customers_json = [ + { + "name": "John Doe", + "email": "john@example.com", + "phone": "555-1234", + "address": "123 Main St" + }, + { + "name": "Jane Smith", + "email": "jane@example.com", + "phone": "555-5678", + "address": "456 Oak Ave" + }, + { + "name": "Bob Johnson", + "email": "bob@example.com", + "phone": "555-9012", + "address": "789 Pine Blvd" + }, + { + "name": "Alice Williams", + "email": "alice@example.com", + "phone": "555-3456", + "address": "321 Maple Dr" + } + ] + self.users = customers_json + +def show_customer(user: dict): + return rx.table.row( + rx.table.cell(user["name"]), + rx.table.cell(user["email"]), + rx.table.cell(user["phone"]), + rx.table.cell(user["address"]), + ) + +def loading_data_table_example(): + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState.users, show_customer)), + on_mount=DatabaseTableState.load_entries, + width="100%", + margin_bottom="1em", +) +``` + +```python eval +loading_data_table_example() +``` + +```python +from sqlmodel import select + +class DatabaseTableState(rx.State): + + users: list[Customer] = [] + + @rx.event + def load_entries(self): + """Get all users from the database.""" + with rx.session() as session: + self.users = session.exec(select(Customer)).all() + + +def show_customer(user: Customer): + """Show a customer in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.phone), + rx.table.cell(user.address), + ) + +def loading_data_table_example(): + return rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState.users, show_customer)), + on_mount=DatabaseTableState.load_entries, + width="100%", + ) + +``` + +## Filtering (Searching) and Sorting + +In this example we sort and filter the data. + +For sorting the `rx.select` component is used. The data is sorted based on the attributes of the `Customer` class. When a select item is selected, as the `on_change` event trigger is hooked up to the `sort_values` event handler, the data is sorted based on the state variable `sort_value` attribute selected. + +The sorting query gets the `sort_column` based on the state variable `sort_value`, it gets the order using the `asc` function from sql and finally uses the `order_by` function. + +For filtering the `rx.input` component is used. The data is filtered based on the search query entered into the `rx.input` component. When a search query is entered, as the `on_change` event trigger is hooked up to the `filter_values` event handler, the data is filtered based on if the state variable `search_value` is present in any of the data in that specific `Customer`. + +The `%` character before and after `search_value` makes it a wildcard pattern that matches any sequence of characters before or after the `search_value`. `query.where(...)` modifies the existing query to include a filtering condition. The `or_` operator is a logical OR operator that combines multiple conditions. The query will return results that match any of these conditions. `Customer.name.ilike(search_value)` checks if the `name` column of the `Customer` table matches the `search_value` pattern in a case-insensitive manner (`ilike` stands for "case-insensitive like"). + +```python +class Customer(rx.Model, table=True): + """The customer model.""" + + name: str + email: str + phone: str + address: str +``` + +```python exec +class DatabaseTableState2(rx.State): + + # Mock data to simulate database records + _users: list[dict] = [ + {"name": "John Doe", "email": "john@example.com", "phone": "555-1234", "address": "123 Main St"}, + {"name": "Jane Smith", "email": "jane@example.com", "phone": "555-5678", "address": "456 Oak Ave"}, + {"name": "Bob Johnson", "email": "bob@example.com", "phone": "555-9012", "address": "789 Pine Rd"}, + {"name": "Alice Brown", "email": "alice@example.com", "phone": "555-3456", "address": "321 Maple Dr"}, + {"name": "Charlie Wilson", "email": "charlie@example.com", "phone": "555-7890", "address": "654 Elm St"}, + {"name": "Emily Davis", "email": "emily@example.com", "phone": "555-2345", "address": "987 Cedar Ln"}, + ] + + users: list[dict] = [] + sort_value = "" + search_value = "" + + @rx.event + def load_entries(self): + # Start with all users + result = self._users.copy() + + # Apply filtering if search value exists + if self.search_value != "": + search_term = self.search_value.lower() + result = [ + user for user in result + if any(search_term in str(value).lower() for value in user.values()) + ] + + # Apply sorting if sort column is selected + if self.sort_value != "": + result = sorted(result, key=lambda x: x[self.sort_value]) + + self.users = result + + @rx.event + def sort_values(self, sort_value): + self.sort_value = sort_value + yield DatabaseTableState2.load_entries() + + @rx.event + def filter_values(self, search_value): + self.search_value = search_value + yield DatabaseTableState2.load_entries() + + +def show_customer_2(user: dict): + return rx.table.row( + rx.table.cell(user["name"]), + rx.table.cell(user["email"]), + rx.table.cell(user["phone"]), + rx.table.cell(user["address"]), + ) + +def loading_data_table_example_2(): + return rx.vstack( + rx.select( + ["name", "email", "phone", "address"], + placeholder="Sort By: Name", + on_change= lambda value: DatabaseTableState2.sort_values(value), + ), + rx.input( + placeholder="Search here...", + on_change= lambda value: DatabaseTableState2.filter_values(value), + ), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState2.users, show_customer_2)), + on_mount=DatabaseTableState2.load_entries, + width="100%", + ), + width="100%", + margin_bottom="1em", +) +``` + +```python eval +loading_data_table_example_2() +``` + +```python +from sqlmodel import select, asc, or_ + + +class DatabaseTableState2(rx.State): + + users: list[Customer] = [] + + sort_value = "" + search_value = "" + + @rx.event + def load_entries(self): + """Get all users from the database.""" + with rx.session() as session: + query = select(Customer) + + if self.search_value != "": + search_value = self.search_value.lower() + query = query.where( + or_( + Customer.name.ilike(search_value), + Customer.email.ilike(search_value), + Customer.phone.ilike(search_value), + Customer.address.ilike(search_value), + ) + ) + + if self.sort_value != "": + sort_column = getattr(Customer, self.sort_value) + order = asc(sort_column) + query = query.order_by(order) + + self.users = session.exec(query).all() + + @rx.event + def sort_values(self, sort_value): + print(sort_value) + self.sort_value = sort_value + self.load_entries() + + @rx.event + def filter_values(self, search_value): + print(search_value) + self.search_value = search_value + self.load_entries() + + +def show_customer(user: Customer): + """Show a customer in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.phone), + rx.table.cell(user.address), + ) + + +def loading_data_table_example2(): + return rx.vstack( + rx.select( + ["name", "email", "phone", "address"], + placeholder="Sort By: Name", + on_change= lambda value: DatabaseTableState2.sort_values(value), + ), + rx.input( + placeholder="Search here...", + on_change= lambda value: DatabaseTableState2.filter_values(value), + ), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState2.users, show_customer)), + on_mount=DatabaseTableState2.load_entries, + width="100%", + ), + width="100%", + ) + +``` + +## Pagination + +Pagination is an important part of database management, especially when working with large datasets. It helps in enabling efficient data retrieval by breaking down results into manageable loads. + +The purpose of this code is to retrieve a specific subset of rows from the `Customer` table based on the specified pagination parameters `offset` and `limit`. + +`query.offset(self.offset)` modifies the query to skip a certain number of rows before returning the results. The number of rows to skip is specified by `self.offset`. + +`query.limit(self.limit)` modifies the query to limit the number of rows returned. The maximum number of rows to return is specified by `self.limit`. + +```python exec +class DatabaseTableState3(rx.State): + + _mock_data: list[Customer] = [ + Customer(name="John Doe", email="john@example.com", phone="555-1234", address="123 Main St"), + Customer(name="Jane Smith", email="jane@example.com", phone="555-5678", address="456 Oak Ave"), + Customer(name="Bob Johnson", email="bob@example.com", phone="555-9012", address="789 Pine Rd"), + Customer(name="Alice Brown", email="alice@example.com", phone="555-3456", address="321 Maple Dr"), + Customer(name="Charlie Wilson", email="charlie@example.com", phone="555-7890", address="654 Elm St"), + Customer(name="Emily Davis", email="emily@example.com", phone="555-2345", address="987 Cedar Ln"), + ] + users: list[Customer] = [] + + total_items: int + offset: int = 0 + limit: int = 3 + + @rx.var(cache=True) + def page_number(self) -> int: + return ( + (self.offset // self.limit) + + 1 + + (1 if self.offset % self.limit else 0) + ) + + @rx.var(cache=True) + def total_pages(self) -> int: + return self.total_items // self.limit + ( + 1 if self.total_items % self.limit else 0 + ) + + @rx.event + def prev_page(self): + self.offset = max(self.offset - self.limit, 0) + self.load_entries() + + @rx.event + def next_page(self): + if self.offset + self.limit < self.total_items: + self.offset += self.limit + self.load_entries() + + def _get_total_items(self, session): + self.total_items = session.exec(select(func.count(Customer.id))).one() + + @rx.event + def load_entries(self): + self.users = self._mock_data[self.offset:self.offset + self.limit] + self.total_items = len(self._mock_data) + + +def show_customer(user: Customer): + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.phone), + rx.table.cell(user.address), + ) + + +def loading_data_table_example3(): + return rx.vstack( + rx.hstack( + rx.button( + "Prev", + on_click=DatabaseTableState3.prev_page, + ), + rx.text( + f"Page {DatabaseTableState3.page_number} / {DatabaseTableState3.total_pages}" + ), + rx.button( + "Next", + on_click=DatabaseTableState3.next_page, + ), + ), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState3.users, show_customer)), + on_mount=DatabaseTableState3.load_entries, + width="100%", + ), + width="100%", + margin_bottom="1em", + ) +``` + +```python eval +loading_data_table_example3() +``` + +```python +from sqlmodel import select, func + + +class DatabaseTableState3(rx.State): + + users: list[Customer] = [] + + total_items: int + offset: int = 0 + limit: int = 3 + + @rx.var(cache=True) + def page_number(self) -> int: + return ( + (self.offset // self.limit) + + 1 + + (1 if self.offset % self.limit else 0) + ) + + @rx.var(cache=True) + def total_pages(self) -> int: + return self.total_items // self.limit + ( + 1 if self.total_items % self.limit else 0 + ) + + @rx.event + def prev_page(self): + self.offset = max(self.offset - self.limit, 0) + self.load_entries() + + @rx.event + def next_page(self): + if self.offset + self.limit < self.total_items: + self.offset += self.limit + self.load_entries() + + def _get_total_items(self, session): + """Return the total number of items in the Customer table.""" + self.total_items = session.exec(select(func.count(Customer.id))).one() + + @rx.event + def load_entries(self): + """Get all users from the database.""" + with rx.session() as session: + query = select(Customer) + + # Apply pagination + query = query.offset(self.offset).limit(self.limit) + + self.users = session.exec(query).all() + self._get_total_items(session) + + +def show_customer(user: Customer): + """Show a customer in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.phone), + rx.table.cell(user.address), + ) + + +def loading_data_table_example3(): + return rx.vstack( + rx.hstack( + rx.button( + "Prev", + on_click=DatabaseTableState3.prev_page, + ), + rx.text( + f"Page {DatabaseTableState3.page_number} / {DatabaseTableState3.total_pages}" + ), + rx.button( + "Next", + on_click=DatabaseTableState3.next_page, + ), + ), + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(DatabaseTableState3.users, show_customer)), + on_mount=DatabaseTableState3.load_entries, + width="100%", + ), + width="100%", + ) + +``` + +## More advanced examples + +The real power of the `rx.table` comes where you are able to visualise, add and edit data live in your app. Check out these apps and code to see how this is done: app: https://customer-data-app.reflex.run code: https://github.com/reflex-dev/templates/tree/main/customer_data_app and code: https://github.com/reflex-dev/templates/tree/main/sales. + +# Download + +Most users will want to download their data after they have got the subset that they would like in their table. + +In this example there are buttons to download the data as a `json` and as a `csv`. + +For the `json` download the `rx.download` is in the frontend code attached to the `on_click` event trigger for the button. This works because if the `Var` is not already a string, it will be converted to a string using `JSON.stringify`. + +For the `csv` download the `rx.download` is in the backend code as an event_handler `download_csv_data`. There is also a helper function `_convert_to_csv` that converts the data in `self.users` to `csv` format. + +```python exec +import io +import csv +import json + +class TableDownloadState(rx.State): + _mock_data: list[Customer] = [ + Customer(name="John Doe", email="john@example.com", phone="555-1234", address="123 Main St"), + Customer(name="Jane Smith", email="jane@example.com", phone="555-5678", address="456 Oak Ave"), + Customer(name="Bob Johnson", email="bob@example.com", phone="555-9012", address="789 Pine Rd"), + ] + users: list[Customer] = [] + + @rx.event + def load_entries(self): + self.users = self._mock_data + + def _convert_to_csv(self) -> str: + if not self.users: + self.load_entries() + + fieldnames = ["id", "name", "email", "phone", "address"] + output = io.StringIO() + writer = csv.DictWriter(output, fieldnames=fieldnames) + writer.writeheader() + for user in self.users: + writer.writerow(user.dict()) + + csv_data = output.getvalue() + output.close() + return csv_data + + + def _convert_to_json(self) -> str: + return json.dumps([u.dict() for u in self._mock_data], indent=2) + + @rx.event + def download_csv_data(self): + csv_data = self._convert_to_csv() + return rx.download( + data=csv_data, + filename="data.csv", + ) + + @rx.event + def download_json_data(self): + json_data = self._convert_to_json() + return rx.download( + data=json_data, + filename="data.json", + ) + +def show_customer(user: Customer): + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.phone), + rx.table.cell(user.address), + ) + +def download_data_table_example(): + return rx.vstack( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(TableDownloadState.users, show_customer)), + width="100%", + on_mount=TableDownloadState.load_entries, + ), + rx.hstack( + rx.button( + "Download as JSON", + on_click=TableDownloadState.download_json_data, + ), + rx.button( + "Download as CSV", + on_click=TableDownloadState.download_csv_data, + ), + spacing="7", + ), + width="100%", + spacing="5", + margin_bottom="1em", + ) + +``` + +```python eval +download_data_table_example() +``` + +```python +import io +import csv +from sqlmodel import select + +class TableDownloadState(rx.State): + + users: list[Customer] = [] + + @rx.event + def load_entries(self): + """Get all users from the database.""" + with rx.session() as session: + self.users = session.exec(select(Customer)).all() + + + def _convert_to_csv(self) -> str: + """Convert the users data to CSV format.""" + + # Make sure to load the entries first + if not self.users: + self.load_entries() + + # Define the CSV file header based on the Customer model's attributes + fieldnames = list(Customer.__fields__) + + # Create a string buffer to hold the CSV data + output = io.StringIO() + writer = csv.DictWriter(output, fieldnames=fieldnames) + writer.writeheader() + for user in self.users: + writer.writerow(user.dict()) + + # Get the CSV data as a string + csv_data = output.getvalue() + output.close() + return csv_data + + @rx.event + def download_csv_data(self): + csv_data = self._convert_to_csv() + return rx.download( + data=csv_data, + filename="data.csv", + ) + + +def show_customer(user: Customer): + """Show a customer in a table row.""" + return rx.table.row( + rx.table.cell(user.name), + rx.table.cell(user.email), + rx.table.cell(user.phone), + rx.table.cell(user.address), + ) + +def download_data_table_example(): + return rx.vstack( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Email"), + rx.table.column_header_cell("Phone"), + rx.table.column_header_cell("Address"), + ), + ), + rx.table.body(rx.foreach(TableDownloadState.users, show_customer)), + width="100%", + on_mount=TableDownloadState.load_entries, + ), + rx.hstack( + rx.button( + "Download as JSON", + on_click=rx.download( + data=TableDownloadState.users, + filename="data.json", + ), + ), + rx.button( + "Download as CSV", + on_click=TableDownloadState.download_csv_data, + ), + spacing="7", + ), + width="100%", + spacing="5", + ) + +``` + +# Real World Example UI + +```python demo +rx.flex( + rx.heading("Your Team"), + rx.text("Invite and manage your team members"), + rx.flex( + rx.input(placeholder="Email Address"), + rx.button("Invite"), + justify="center", + spacing="2", + ), + rx.table.root( + rx.table.body( + rx.table.row( + rx.table.cell(rx.avatar(fallback="DS")), + rx.table.row_header_cell(rx.link("Danilo Sousa")), + rx.table.cell("danilo@example.com"), + rx.table.cell("Developer"), + align="center", + ), + rx.table.row( + rx.table.cell(rx.avatar(fallback="ZA")), + rx.table.row_header_cell(rx.link("Zahra Ambessa")), + rx.table.cell("zahra@example.com"), + rx.table.cell("Admin"), + align="center", + ), + rx.table.row( + rx.table.cell(rx.avatar(fallback="JE")), + rx.table.row_header_cell(rx.link("Jasper Eriksson")), + rx.table.cell("jasper@example.com"), + rx.table.cell("Developer"), + align="center", + ), + ), + width="100%", + ), + width="100%", + direction="column", + spacing="2", +) +``` diff --git a/docs/library/typography/blockquote.md b/docs/library/typography/blockquote.md new file mode 100644 index 00000000000..33b79ac34cc --- /dev/null +++ b/docs/library/typography/blockquote.md @@ -0,0 +1,77 @@ +--- +components: + - rx.blockquote +--- + +```python exec +import reflex as rx +``` + +# Blockquote + +```python demo +rx.blockquote("Perfect typography is certainly the most elusive of all arts.") +``` + +## Size + +Use the `size` prop to control the size of the blockquote. The prop also provides correct line height and corrective letter spacing—as text size increases, the relative line height and letter spacing decrease. + +```python demo +rx.flex( + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="1"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="2"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="3"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="4"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="5"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="6"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="7"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="8"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", size="9"), + direction="column", + spacing="3", +) +``` + +## Weight + +Use the `weight` prop to set the blockquote weight. + +```python demo +rx.flex( + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", weight="light"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", weight="regular"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", weight="medium"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", weight="bold"), + direction="column", + spacing="3", +) +``` + +## Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", color_scheme="indigo"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", color_scheme="cyan"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", color_scheme="crimson"), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", color_scheme="orange"), + direction="column", + spacing="3", +) +``` + +## High Contrast + +Use the `high_contrast` prop to increase color contrast with the background. + +```python demo +rx.flex( + rx.blockquote("Perfect typography is certainly the most elusive of all arts."), + rx.blockquote("Perfect typography is certainly the most elusive of all arts.", high_contrast=True), + direction="column", + spacing="3", +) +``` diff --git a/docs/library/typography/code.md b/docs/library/typography/code.md new file mode 100644 index 00000000000..8db6cf613b2 --- /dev/null +++ b/docs/library/typography/code.md @@ -0,0 +1,110 @@ +--- +components: + - rx.code +--- + +```python exec +import reflex as rx +``` + +# Code + +```python demo +rx.code("console.log()") +``` + +## Size + +Use the `size` prop to control text size. This prop also provides correct line height and corrective letter spacing—as text size increases, the relative line height and letter spacing decrease. + +```python demo +rx.flex( + rx.code("console.log()", size="1"), + rx.code("console.log()", size="2"), + rx.code("console.log()", size="3"), + rx.code("console.log()", size="4"), + rx.code("console.log()", size="5"), + rx.code("console.log()", size="6"), + rx.code("console.log()", size="7"), + rx.code("console.log()", size="8"), + rx.code("console.log()", size="9"), + direction="column", + spacing="3", + align="start", +) +``` + +## Weight + +Use the `weight` prop to set the text weight. + +```python demo +rx.flex( + rx.code("console.log()", weight="light"), + rx.code("console.log()", weight="regular"), + rx.code("console.log()", weight="medium"), + rx.code("console.log()", weight="bold"), + direction="column", + spacing="3", +) +``` + +## Variant + +Use the `variant` prop to control the visual style. + +```python demo +rx.flex( + rx.code("console.log()", variant="solid"), + rx.code("console.log()", variant="soft"), + rx.code("console.log()", variant="outline"), + rx.code("console.log()", variant="ghost"), + direction="column", + spacing="2", + align="start", +) +``` + +## Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.code("console.log()", color_scheme="indigo"), + rx.code("console.log()", color_scheme="crimson"), + rx.code("console.log()", color_scheme="orange"), + rx.code("console.log()", color_scheme="cyan"), + direction="column", + spacing="2", + align="start", +) +``` + +## High Contrast + +Use the `high_contrast` prop to increase color contrast with the background. + +```python demo +rx.flex( + rx.flex( + rx.code("console.log()", variant="solid"), + rx.code("console.log()", variant="soft"), + rx.code("console.log()", variant="outline"), + rx.code("console.log()", variant="ghost"), + direction="column", + align="start", + spacing="2", + ), + rx.flex( + rx.code("console.log()", variant="solid", high_contrast=True), + rx.code("console.log()", variant="soft", high_contrast=True), + rx.code("console.log()", variant="outline", high_contrast=True), + rx.code("console.log()", variant="ghost", high_contrast=True), + direction="column", + align="start", + spacing="2", + ), + spacing="3", +) +``` diff --git a/docs/library/typography/em.md b/docs/library/typography/em.md new file mode 100644 index 00000000000..a977335333c --- /dev/null +++ b/docs/library/typography/em.md @@ -0,0 +1,16 @@ +--- +components: + - rx.text.em +--- + +```python exec +import reflex as rx +``` + +# Em (Emphasis) + +Marks text to stress emphasis. + +```python demo +rx.text("We ", rx.text.em("had"), " to do something about it.") +``` diff --git a/docs/library/typography/heading.md b/docs/library/typography/heading.md new file mode 100644 index 00000000000..7d0a3737328 --- /dev/null +++ b/docs/library/typography/heading.md @@ -0,0 +1,152 @@ +--- +components: + - rx.heading +--- + +```python exec +import reflex as rx +``` + +# Heading + +```python demo +rx.heading("The quick brown fox jumps over the lazy dog.") +``` + +## As another element + +Use the `as_` prop to change the heading level. This prop is purely semantic and does not change the visual appearance. + +```python demo +rx.flex( + rx.heading("Level 1", as_="h1"), + rx.heading("Level 2", as_="h2"), + rx.heading("Level 3", as_="h3"), + direction="column", + spacing="3", +) +``` + +## Size + +Use the `size` prop to control the size of the heading. The prop also provides correct line height and corrective letter spacing—as text size increases, the relative line height and letter spacing decrease + +```python demo +rx.flex( + rx.heading("The quick brown fox jumps over the lazy dog.", size="1"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="2"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="3"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="4"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="5"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="6"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="7"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="8"), + rx.heading("The quick brown fox jumps over the lazy dog.", size="9"), + direction="column", + spacing="3", +) +``` + +## Weight + +Use the `weight` prop to set the text weight. + +```python demo +rx.flex( + rx.heading("The quick brown fox jumps over the lazy dog.", weight="light"), + rx.heading("The quick brown fox jumps over the lazy dog.", weight="regular"), + rx.heading("The quick brown fox jumps over the lazy dog.", weight="medium"), + rx.heading("The quick brown fox jumps over the lazy dog.", weight="bold"), + direction="column", + spacing="3", +) +``` + +## Align + +Use the `align` prop to set text alignment. + +```python demo +rx.flex( + rx.heading("Left-aligned", align="left"), + rx.heading("Center-aligned", align="center"), + rx.heading("Right-aligned", align="right"), + direction="column", + spacing="3", + width="100%", +) +``` + +## Trim + +Use the `trim` prop to trim the leading space at the start, end, or both sides of the text. + +```python demo +rx.flex( + rx.heading("Without Trim", + trim="normal", + style={"background": "var(--gray-a2)", + "border_top": "1px dashed var(--gray-a7)", + "border_bottom": "1px dashed var(--gray-a7)",} + ), + rx.heading("With Trim", + trim="both", + style={"background": "var(--gray-a2)", + "border_top": "1px dashed var(--gray-a7)", + "border_bottom": "1px dashed var(--gray-a7)",} + ), + direction="column", + spacing="3", +) +``` + +Trimming the leading is useful when dialing in vertical spacing in cards or other “boxy” components. Otherwise, padding looks larger on top and bottom than on the sides. + +```python demo +rx.flex( + rx.box( + rx.heading("Without trim", margin_bottom="4px", size="3",), + rx.text("The goal of typography is to relate font size, line height, and line width in a proportional way that maximizes beauty and makes reading easier and more pleasant."), + style={"background": "var(--gray-a2)", + "border": "1px dashed var(--gray-a7)",}, + padding="16px", + ), + rx.box( + rx.heading("With trim", margin_bottom="4px", size="3", trim="start"), + rx.text("The goal of typography is to relate font size, line height, and line width in a proportional way that maximizes beauty and makes reading easier and more pleasant."), + style={"background": "var(--gray-a2)", + "border": "1px dashed var(--gray-a7)",}, + padding="16px", + ), + direction="column", + spacing="3", +) +``` + +## Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="indigo"), + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="cyan"), + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="crimson"), + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="orange"), + direction="column", +) +``` + +## High Contrast + +Use the `high_contrast` prop to increase color contrast with the background. + +```python demo +rx.flex( + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="indigo", high_contrast=True), + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="cyan", high_contrast=True), + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="crimson", high_contrast=True), + rx.heading("The quick brown fox jumps over the lazy dog.", color_scheme="orange", high_contrast=True), + direction="column", +) +``` diff --git a/docs/library/typography/kbd.md b/docs/library/typography/kbd.md new file mode 100644 index 00000000000..e20078b75dd --- /dev/null +++ b/docs/library/typography/kbd.md @@ -0,0 +1,36 @@ +--- +components: + - rx.text.kbd +--- + +```python exec +import reflex as rx +``` + +# rx.text.kbd (Keyboard) + +Represents keyboard input or a hotkey. + +```python demo +rx.text.kbd("Shift + Tab") +``` + +## Size + +Use the `size` prop to control text size. This prop also provides correct line height and corrective letter spacing—as text size increases, the relative line height and letter spacing decrease. + +```python demo +rx.flex( + rx.text.kbd("Shift + Tab", size="1"), + rx.text.kbd("Shift + Tab", size="2"), + rx.text.kbd("Shift + Tab", size="3"), + rx.text.kbd("Shift + Tab", size="4"), + rx.text.kbd("Shift + Tab", size="5"), + rx.text.kbd("Shift + Tab", size="6"), + rx.text.kbd("Shift + Tab", size="7"), + rx.text.kbd("Shift + Tab", size="8"), + rx.text.kbd("Shift + Tab", size="9"), + direction="column", + spacing="3", +) +``` diff --git a/docs/library/typography/link.md b/docs/library/typography/link.md new file mode 100644 index 00000000000..8f4b0a73d90 --- /dev/null +++ b/docs/library/typography/link.md @@ -0,0 +1,147 @@ +--- +components: + - rx.link +--- + +```python exec +import reflex as rx +from pcweb.pages.docs import api_reference +``` + +# Link + +Links are accessible elements used primarily for navigation. Use the `href` prop to specify the location for the link to navigate to. + +```python demo +rx.link("Reflex Home Page.", href="https://reflex.dev/") +``` + +You can also provide local links to other pages in your project without writing the full url. + +```python demo +rx.link("Example", href="/docs/library",) +``` + +The `link` component can be used to wrap other components to make them link to other pages. + +```python demo +rx.link(rx.button("Example"), href="https://reflex.dev/") +``` + +You can also create anchors to link to specific parts of a page using the `id` prop. + +```python demo +rx.box("Example", id="example") +``` + +To reference an anchor, you can use the `href` prop of the `link` component. The `href` should be in the format of the page you want to link to followed by a # and the id of the anchor. + +```python demo +rx.link("Example", href="/docs/library/typography/link#example") +``` + +```md alert info +# Redirecting the user using State + +It is also possible to redirect the user to a new path within the application, using `rx.redirect()`. Check out the docs [here]({api_reference.special_events.path}). +``` + +# Style + +## Size + +Use the `size` prop to control the size of the link. The prop also provides correct line height and corrective letter spacing—as text size increases, the relative line height and letter spacing decrease. + +```python demo +rx.flex( + rx.link("The quick brown fox jumps over the lazy dog.", size="1"), + rx.link("The quick brown fox jumps over the lazy dog.", size="2"), + rx.link("The quick brown fox jumps over the lazy dog.", size="3"), + rx.link("The quick brown fox jumps over the lazy dog.", size="4"), + rx.link("The quick brown fox jumps over the lazy dog.", size="5"), + rx.link("The quick brown fox jumps over the lazy dog.", size="6"), + rx.link("The quick brown fox jumps over the lazy dog.", size="7"), + rx.link("The quick brown fox jumps over the lazy dog.", size="8"), + rx.link("The quick brown fox jumps over the lazy dog.", size="9"), + direction="column", + spacing="3", +) +``` + +## Weight + +Use the `weight` prop to set the text weight. + +```python demo +rx.flex( + rx.link("The quick brown fox jumps over the lazy dog.", weight="light"), + rx.link("The quick brown fox jumps over the lazy dog.", weight="regular"), + rx.link("The quick brown fox jumps over the lazy dog.", weight="medium"), + rx.link("The quick brown fox jumps over the lazy dog.", weight="bold"), + direction="column", + spacing="3", +) +``` + +## Trim + +Use the `trim` prop to trim the leading space at the start, end, or both sides of the rendered text. + +```python demo +rx.flex( + rx.link("Without Trim", + trim="normal", + style={"background": "var(--gray-a2)", + "border_top": "1px dashed var(--gray-a7)", + "border_bottom": "1px dashed var(--gray-a7)",} + ), + rx.link("With Trim", + trim="both", + style={"background": "var(--gray-a2)", + "border_top": "1px dashed var(--gray-a7)", + "border_bottom": "1px dashed var(--gray-a7)",} + ), + direction="column", + spacing="3", +) +``` + +## Underline + +Use the `underline` prop to manage the visibility of the underline affordance. It defaults to `auto`. + +```python demo +rx.flex( + rx.link("The quick brown fox jumps over the lazy dog.", underline="auto"), + rx.link("The quick brown fox jumps over the lazy dog.", underline="hover"), + rx.link("The quick brown fox jumps over the lazy dog.", underline="always"), + direction="column", + spacing="3", +) +``` + +## Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.link("The quick brown fox jumps over the lazy dog.", color_scheme="indigo"), + rx.link("The quick brown fox jumps over the lazy dog.", color_scheme="cyan"), + rx.link("The quick brown fox jumps over the lazy dog.", color_scheme="crimson"), + rx.link("The quick brown fox jumps over the lazy dog.", color_scheme="orange"), + direction="column", +) +``` + +## High Contrast + +Use the `high_contrast` prop to increase color contrast with the background. + +```python demo +rx.flex( + rx.link("The quick brown fox jumps over the lazy dog."), + rx.link("The quick brown fox jumps over the lazy dog.", high_contrast=True), + direction="column", +) +``` diff --git a/docs/library/typography/markdown.md b/docs/library/typography/markdown.md new file mode 100644 index 00000000000..19f19a07725 --- /dev/null +++ b/docs/library/typography/markdown.md @@ -0,0 +1,196 @@ +--- +components: + - rx.markdown +--- + +```python exec +import reflex as rx +``` + +# Markdown + +The `rx.markdown` component can be used to render markdown text. +It is based on [Github Flavored Markdown](https://github.github.com/gfm/). + +```python demo +rx.vstack( + rx.markdown("# Hello World!"), + rx.markdown("## Hello World!"), + rx.markdown("### Hello World!"), + rx.markdown("Support us on [Github](https://github.com/reflex-dev/reflex)."), + rx.markdown("Use `reflex deploy` to deploy your app with **a single command**."), +) +``` + +## Math Equations + +You can render math equations using LaTeX. +For inline equations, surround the equation with `$`: + +```python demo +rx.markdown("Pythagorean theorem: $a^2 + b^2 = c^2$.") +``` + +## Syntax Highlighting + +You can render code blocks with syntax highlighting using the \`\`\`\{language} syntax: + +````python demo +rx.markdown( +r""" +```python +import reflex as rx +from .pages import index + +app = rx.App() +app.add_page(index) +``` +""" +) +```` + +## Tables + +You can render tables using the `|` syntax: + +```python demo +rx.markdown( + """ +| Syntax | Description | +| ----------- | ----------- | +| Header | Title | +| Paragraph | Text | +""" +) +``` + +## Plugins + +Plugins can be used to extend the functionality of the markdown renderer. + +By default Reflex uses the following plugins: + +- `remark-gfm` for Github Flavored Markdown support (`use_gfm`). +- `remark-math` and `rehype-katex` for math equation support (`use_math`, `use_katex`). +- `rehype-unwrap-images` to remove paragraph tags around images (`use_unwrap_images`). +- `rehype-raw` to render raw HTML in markdown (`use_raw`). NOTE: in a future release this will be disabled by default for security reasons. + +These default plugins can be disabled by passing `use_[plugin_name]=False` to the `rx.markdown` component. For example, to disable raw HTML rendering, use `rx.markdown(..., use_raw=False)`. + +## Arbitrary Plugins + +You can also add arbitrary remark or rehype plugins using the `remark_plugins` +and `rehype_plugins` props in conjunction with the `rx.markdown.plugin` helper. + +`rx.markdown.plugin` takes two arguments: + +1. The npm package name and version of the plugin (e.g. `remark-emoji@5.0.2`). +2. The named export to use from the plugin (e.g. `remarkEmoji`). + +### Remark Plugin Example + +For example, to add support for emojis using the `remark-emoji` plugin: + +```python demo +rx.markdown( + "Hello :smile:! :rocket: :tada:", + remark_plugins=[ + rx.markdown.plugin("remark-emoji@5.0.2", "remarkEmoji"), + ], +) +``` + +### Rehype Plugin Example + +To make `rehype-raw` safer for untrusted HTML input we can use `rehype-sanitize`, which defaults to a safe schema similar to that used by Github. + +```python demo +rx.markdown( + """Here is some **bold** text and a .""", + use_raw=True, + rehype_plugins=[ + rx.markdown.plugin("rehype-sanitize@5.0.1", "rehypeSanitize"), + ], +) +``` + +### Plugin Options + +Both `remark_plugins` and `rehype_plugins` accept a heterogeneous list of `plugin` +or tuple of `(plugin, options)` in case the plugin requires some kind of special +configuration. + +For example, `rehype-slug` is a simple plugin that adds ID attributes to the +headings, but the `rehype-autolink-headings` plugin accepts options to specify +how to render the links to those anchors. + +```python demo +rx.markdown( + """ +# Heading 1 +## Heading 2 +""", + rehype_plugins=[ + rx.markdown.plugin("rehype-slug@6.0.0", "rehypeSlug"), + ( + rx.markdown.plugin("rehype-autolink-headings@7.1.0", "rehypeAutolinkHeadings"), + { + "behavior": "wrap", + "properties": { + "className": ["heading-link"], + }, + }, + ), + ], +) +``` + +## Component Map + +You can specify which components to use for rendering markdown elements using the +`component_map` prop. + +Each key in the `component_map` prop is a markdown element, and the value is +a function that takes the text of the element as input and returns a Reflex component. + +```md alert +The `pre` and `a` tags are special cases. In addition to the `text`, they also receive a `props` argument containing additional props for the component. +``` + +````python demo exec +component_map = { + "h1": lambda text: rx.heading(text, size="5", margin_y="1em"), + "h2": lambda text: rx.heading(text, size="3", margin_y="1em"), + "h3": lambda text: rx.heading(text, size="1", margin_y="1em"), + "p": lambda text: rx.text(text, color="green", margin_y="1em"), + "code": lambda text: rx.code(text, color="purple"), + "pre": lambda text, **props: rx.code_block(text, **props, theme=rx.code_block.themes.dark, margin_y="1em"), + "a": lambda text, **props: rx.link(text, **props, color="blue", _hover={"color": "red"}), +} + +def index(): + return rx.box( + rx.markdown( +r""" +# Hello World! + +## This is a Subheader + +### And Another Subheader + +Here is some `code`: + +```python +import reflex as rx + +component = rx.text("Hello World!") +``` + +And then some more text here, +followed by a link to +[Reflex](https://reflex.dev/). +""", + component_map=component_map, +) + ) +```` diff --git a/docs/library/typography/quote.md b/docs/library/typography/quote.md new file mode 100644 index 00000000000..aefedc8abc5 --- /dev/null +++ b/docs/library/typography/quote.md @@ -0,0 +1,19 @@ +--- +components: + - rx.text.quote +--- + +```python exec +import reflex as rx +``` + +# Quote + +A short inline quotation. + +```python demo +rx.text("His famous quote, ", + rx.text.quote("Styles come and go. Good design is a language, not a style"), + ", elegantly sums up Massimo’s philosophy of design." + ) +``` diff --git a/docs/library/typography/strong.md b/docs/library/typography/strong.md new file mode 100644 index 00000000000..7ebfc9132df --- /dev/null +++ b/docs/library/typography/strong.md @@ -0,0 +1,16 @@ +--- +components: + - rx.text.strong +--- + +```python exec +import reflex as rx +``` + +# Strong + +Marks text to signify strong importance. + +```python demo +rx.text("The most important thing to remember is, ", rx.text.strong("stay positive"), ".") +``` diff --git a/docs/library/typography/text.md b/docs/library/typography/text.md new file mode 100644 index 00000000000..73f9e42df98 --- /dev/null +++ b/docs/library/typography/text.md @@ -0,0 +1,204 @@ +--- +components: + - rx.text + - rx.text.em +--- + +```python exec +import reflex as rx +``` + +# Text + +```python demo +rx.text("The quick brown fox jumps over the lazy dog.") +``` + +## As another element + +Use the `as_` prop to render text as a `p`, `label`, `div` or `span`. This prop is purely semantic and does not alter visual appearance. + +```python demo +rx.flex( + rx.text("This is a ", rx.text.strong("paragraph"), " element.", as_="p"), + rx.text("This is a ", rx.text.strong("label"), " element.", as_="label"), + rx.text("This is a ", rx.text.strong("div"), " element.", as_="div"), + rx.text("This is a ", rx.text.strong("span"), " element.", as_="span"), + direction="column", + spacing="3", +) +``` + +## Size + +Use the `size` prop to control text size. This prop also provides correct line height and corrective letter spacing—as text size increases, the relative line height and letter spacing decrease. + +```python demo +rx.flex( + rx.text("The quick brown fox jumps over the lazy dog.", size="1"), + rx.text("The quick brown fox jumps over the lazy dog.", size="2"), + rx.text("The quick brown fox jumps over the lazy dog.", size="3"), + rx.text("The quick brown fox jumps over the lazy dog.", size="4"), + rx.text("The quick brown fox jumps over the lazy dog.", size="5"), + rx.text("The quick brown fox jumps over the lazy dog.", size="6"), + rx.text("The quick brown fox jumps over the lazy dog.", size="7"), + rx.text("The quick brown fox jumps over the lazy dog.", size="8"), + rx.text("The quick brown fox jumps over the lazy dog.", size="9"), + direction="column", + spacing="3", +) +``` + +Sizes 2–4 are designed to work well for long-form content. Sizes 1–3 are designed to work well for UI labels. + +## Weight + +Use the `weight` prop to set the text weight. + +```python demo +rx.flex( + rx.text("The quick brown fox jumps over the lazy dog.", weight="light", as_="div"), + rx.text("The quick brown fox jumps over the lazy dog.", weight="regular", as_="div"), + rx.text("The quick brown fox jumps over the lazy dog.", weight="medium", as_="div"), + rx.text("The quick brown fox jumps over the lazy dog.", weight="bold", as_="div"), + direction="column", + spacing="3", +) +``` + +## Align + +Use the `align` prop to set text alignment. + +```python demo +rx.flex( + rx.text("Left-aligned", align="left", as_="div"), + rx.text("Center-aligned", align="center", as_="div"), + rx.text("Right-aligned", align="right", as_="div"), + direction="column", + spacing="3", + width="100%", +) +``` + +## Trim + +Use the `trim` prop to trim the leading space at the start, end, or both sides of the text box. + +```python demo +rx.flex( + rx.text("Without Trim", + trim="normal", + style={"background": "var(--gray-a2)", + "border_top": "1px dashed var(--gray-a7)", + "border_bottom": "1px dashed var(--gray-a7)",} + ), + rx.text("With Trim", + trim="both", + style={"background": "var(--gray-a2)", + "border_top": "1px dashed var(--gray-a7)", + "border_bottom": "1px dashed var(--gray-a7)",} + ), + direction="column", + spacing="3", +) +``` + +Trimming the leading is useful when dialing in vertical spacing in cards or other “boxy” components. Otherwise, padding looks larger on top and bottom than on the sides. + +```python demo +rx.flex( + rx.box( + rx.heading("Without trim", margin_bottom="4px", size="3",), + rx.text("The goal of typography is to relate font size, line height, and line width in a proportional way that maximizes beauty and makes reading easier and more pleasant."), + style={"background": "var(--gray-a2)", + "border": "1px dashed var(--gray-a7)",}, + padding="16px", + ), + rx.box( + rx.heading("With trim", margin_bottom="4px", size="3", trim="start"), + rx.text("The goal of typography is to relate font size, line height, and line width in a proportional way that maximizes beauty and makes reading easier and more pleasant."), + style={"background": "var(--gray-a2)", + "border": "1px dashed var(--gray-a7)",}, + padding="16px", + ), + direction="column", + spacing="3", +) +``` + +## Color + +Use the `color_scheme` prop to assign a specific color, ignoring the global theme. + +```python demo +rx.flex( + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="indigo"), + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="cyan"), + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="crimson"), + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="orange"), + direction="column", +) +``` + +## High Contrast + +Use the `high_contrast` prop to increase color contrast with the background. + +```python demo +rx.flex( + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="indigo", high_contrast=True), + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="cyan", high_contrast=True), + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="crimson", high_contrast=True), + rx.text("The quick brown fox jumps over the lazy dog.", color_scheme="orange", high_contrast=True), + direction="column", +) +``` + +## With formatting + +Compose `Text` with formatting components to add emphasis and structure to content. + +```python demo +rx.text( + "Look, such a helpful ", + rx.link("link", href="#"), + ", an ", + rx.text.em("italic emphasis"), + " a piece of computer ", + rx.code("code"), + ", and even a hotkey combination ", + rx.text.kbd("⇧⌘A"), + " within the text.", + size="5", +) +``` + +## Preformmatting + +By Default, the browser renders multiple white spaces into one. To preserve whitespace, use the `white_space = "pre"` css prop. + +```python demo +rx.hstack( + rx.text("This is not pre formatted"), + rx.text("This is pre formatted", white_space="pre"), +) +``` + +## With form controls + +Composing `text` with a form control like `checkbox`, `radiogroup`, or `switch` automatically centers the control with the first line of text, even when the text is multi-line. + +```python demo +rx.box( + rx.text( + rx.flex( + rx.checkbox(default_checked=True), + "I understand that these documents are confidential and cannot be shared with a third party.", + ), + as_="label", + size="3", + ), + style={"max_width": 300}, +) +``` diff --git a/docs/pages/dynamic_routing.md b/docs/pages/dynamic_routing.md new file mode 100644 index 00000000000..72e555b62cc --- /dev/null +++ b/docs/pages/dynamic_routing.md @@ -0,0 +1,110 @@ +```python exec +import reflex as rx +from pcweb import constants, styles +``` + +# Dynamic Routes + +Dynamic routes in Reflex allow you to handle varying URL structures, enabling you to create flexible +and adaptable web applications. This section covers regular dynamic routes, catch-all routes, +and optional catch-all routes, each with detailed examples. + +## Regular Dynamic Routes + +Regular dynamic routes in Reflex allow you to match specific segments in a URL dynamically. A regular dynamic route is defined by square brackets in a route string / url pattern. For example `/users/[id]` or `/products/[category]`. These dynamic route arguments can be accessed through a state var. For the examples above they would be `rx.State.id` and `rx.State.category` respectively. + +```md alert info +# Why is the state var accessed as `rx.State.id`? + +The dynamic route arguments are accessible as `rx.State.id` and `rx.State.category` here as the var is added to the root state, so that it is accessible from any state. +``` + +Example: + +```python +@rx.page(route='/post/[pid]') +def post(): + '''A page that updates based on the route.''' + # Displays the dynamic part of the URL, the post ID + return rx.heading(rx.State.pid) + +app = rx.App() +``` + +The [pid] part in the route is a dynamic segment, meaning it can match any value provided in the URL. For instance, `/post/5`, `/post/10`, or `/post/abc` would all match this route. + +If a user navigates to `/post/5`, `State.post_id` will return `5`, and the page will display `5` as the heading. If the URL is `/post/xyz`, it will display `xyz`. If the URL is `/post/` without any additional parameter, it will display `""`. + +### Adding Dynamic Routes + +Adding dynamic routes uses the `add_page` method like any other page. The only difference is that the route string contains dynamic segments enclosed in square brackets. + +If you are using the `app.add_page` method to define pages, it is necessary to add the dynamic routes first, especially if they use the same function as a non dynamic route. + +For example the code snippet below will: + +```python +app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load) +app.add_page(index, route="/static/x", on_load=DynamicState.on_load) +app.add_page(index) +``` + +But if we switch the order of adding the pages, like in the example below, it will not work: + +```python +app.add_page(index, route="/static/x", on_load=DynamicState.on_load) +app.add_page(index) +app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load) +``` + +## Catch-All Routes + +Catch-all routes in Reflex allow you to match any number of segments in a URL dynamically. + +Example: + +```python +class State(rx.State): + @rx.var + def user_post(self) -> str: + args = self.router.page.params + usernames = args.get('splat', []) + return f"Posts by \{', '.join(usernames)}" + +@rx.page(route='/users/[id]/posts/[[...splat]]') +def post(): + return rx.center( + rx.text(State.user_post) + ) + + +app = rx.App() +``` + +In this case, the `...splat` catch-all pattern captures any number of segments after +`/users/`, allowing URLs like `/users/2/posts/john/` and `/users/1/posts/john/doe/` to match the route. + +```md alert +# Catch-all routes must be named `splat` and be placed at the end of the URL pattern to ensure proper route matching. +``` + +### Routes Validation Table + +| Route Pattern | Example URl | valid | +| :----------------------------------------------- | :---------------------------------------------- | ------: | +| `/users/posts` | `/users/posts` | valid | +| `/products/[category]` | `/products/electronics` | valid | +| `/users/[username]/posts/[id]` | `/users/john/posts/5` | valid | +| `/users/[[...splat]]/posts` | `/users/john/posts` | invalid | +| | `/users/john/doe/posts` | invalid | +| `/users/[[...splat]]` | `/users/john/` | valid | +| | `/users/john/doe` | valid | +| `/products/[category]/[[...splat]]` | `/products/electronics/laptops` | valid | +| | `/products/electronics/laptops/lenovo` | valid | +| `/products/[category]/[[...splat]]` | `/products/electronics` | valid | +| | `/products/electronics/laptops` | valid | +| | `/products/electronics/laptops/lenovo` | valid | +| | `/products/electronics/laptops/lenovo/thinkpad` | valid | +| `/products/[category]/[[...splat]]/[[...splat]]` | `/products/electronics/laptops` | invalid | +| | `/products/electronics/laptops/lenovo` | invalid | +| | `/products/electronics/laptops/lenovo/thinkpad` | invalid | diff --git a/docs/pages/overview.md b/docs/pages/overview.md new file mode 100644 index 00000000000..ac19bdd74d2 --- /dev/null +++ b/docs/pages/overview.md @@ -0,0 +1,240 @@ +```python exec +import reflex as rx +from pcweb import constants, styles +from pcweb.pages import docs +from pcweb.pages.docs import api_reference, library +``` + +# Pages + +Pages map components to different URLs in your app. This section covers creating pages, handling URL arguments, accessing query parameters, managing page metadata, and handling page load events. + +## Adding a Page + +You can create a page by defining a function that returns a component. +By default, the function name will be used as the route, but you can also specify a route. + +```python +def index(): + return rx.text('Root Page') + +def about(): + return rx.text('About Page') + + +def custom(): + return rx.text('Custom Route') + +app = rx.App() + +app.add_page(index) +app.add_page(about) +app.add_page(custom, route="/custom-route") +``` + +In this example we create three pages: + +- `index` - The root route, available at `/` +- `about` - available at `/about` +- `custom` - available at `/custom-route` + +```md alert +# Index is a special exception where it is available at both `/` and `/index`. All other pages are only available at their specified route. +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=3853&end=4083 +# Video: Pages and URL Routes +``` + +## Page Decorator + +You can also use the `@rx.page` decorator to add a page. + +```python +@rx.page(route='/', title='My Beautiful App') +def index(): + return rx.text('A Beautiful App') +``` + +This is equivalent to calling `app.add_page` with the same arguments. + +```md alert warning +# Remember to import the modules defining your decorated pages. + +This is necessary for the pages to be registered with the app. + +You can directly import the module or import another module that imports the decorated pages. +``` + +## Navigating Between Pages + +### Links + +[Links]({library.typography.link.path}) are accessible elements used primarily for navigation. Use the `href` prop to specify the location for the link to navigate to. + +```python demo +rx.link("Reflex Home Page.", href="https://reflex.dev/") +``` + +You can also provide local links to other pages in your project without writing the full url. + +```python demo +rx.link("Example", href="/docs/library") +``` + +To open the link in a new tab, set the `is_external` prop to `True`. + +```python demo +rx.link("Open in new tab", href="https://reflex.dev/", is_external=True) +``` + +Check out the [link docs]({library.typography.link.path}) to learn more. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=4083&end=4423 +# Video: Link-based Navigation +``` + +### Redirect + +Redirect the user to a new path within the application using `rx.redirect()`. + +- `path`: The destination path or URL to which the user should be redirected. +- `external`: If set to True, the redirection will open in a new tab. Defaults to `False`. + +```python demo +rx.vstack( + rx.button("open in tab", on_click=rx.redirect("/docs/api-reference/special_events")), + rx.button("open in new tab", on_click=rx.redirect('https://github.com/reflex-dev/reflex/', is_external=True)) +) +``` + +Redirect can also be run from an event handler in State, meaning logic can be added behind it. It is necessary to `return` the `rx.redirect()`. + +```python demo exec +class Redirect2ExampleState(rx.State): + redirect_to_org: bool = False + + @rx.event + def change_redirect(self): + self.redirect_to_org = not self.redirect_to_org + + @rx.var + def url(self) -> str: + return 'https://github.com/reflex-dev/' if self.redirect_to_org else 'https://github.com/reflex-dev/reflex/' + + @rx.event + def change_page(self): + return rx.redirect(self.url, is_external=True) + +def redirect_example(): + return rx.vstack( + rx.text(f"{Redirect2ExampleState.url}"), + rx.button("Change redirect location", on_click=Redirect2ExampleState.change_redirect), + rx.button("Redirect to new page in State", on_click=Redirect2ExampleState.change_page), + + ) +``` + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=4423&end=4903 +# Video: Redirecting to a New Page +``` + +## Nested Routes + +Pages can also have nested routes. + +```python +def nested_page(): + return rx.text('Nested Page') + +app = rx.App() +app.add_page(nested_page, route='/nested/page') +``` + +This component will be available at `/nested/page`. + +## Page Metadata + +```python exec + +import reflex as rx + +meta_data = ( +""" +@rx.page( + title='My Beautiful App', + description='A beautiful app built with Reflex', + image='https://web.reflex-assets.dev/other/logo.jpg', + meta=meta, +) +def index(): + return rx.text('A Beautiful App') + +@rx.page(title='About Page') +def about(): + return rx.text('About Page') + + +meta = [ + {'name': 'theme_color', 'content': '#FFFFFF'}, + {'char_set': 'UTF-8'}, + {'property': 'og:url', 'content': 'url'}, +] + +app = rx.App() +""" + +) + +``` + +You can add page metadata such as: + +- The title to be shown in the browser tab +- The description as shown in search results +- The preview image to be shown when the page is shared on social media +- Any additional metadata + +```python +{meta_data} +``` + +## Getting the Current Page + +You can access the current page from the `router` attribute in any state. See the [router docs]({docs.utility_methods.router_attributes.path}) for all available attributes. + +```python +class State(rx.State): + def some_method(self): + current_page_route = self.router.page.path + current_page_url = self.router.page.raw_path + # ... Your logic here ... +``` + +The `router.page.path` attribute allows you to obtain the path of the current page from the router data, +for [dynamic pages]({docs.pages.dynamic_routing.path}) this will contain the slug rather than the actual value used to load the page. + +To get the actual URL displayed in the browser, use `router.page.raw_path`. This +will contain all query parameters and dynamic path segments. + +In the above example, `current_page_route` will contain the route pattern (e.g., `/posts/[id]`), while `current_page_url` +will contain the actual URL (e.g., `/posts/123`). + +To get the full URL, access the same attributes with `full_` prefix. + +Example: + +```python +class State(rx.State): + @rx.var + def current_url(self) -> str: + return self.router.page.full_raw_path + +def index(): + return rx.text(State.current_url) + +app = rx.App() +app.add_page(index, route='/posts/[id]') +``` + +In this example, running on `localhost` should display `http://localhost:3000/posts/123/` diff --git a/docs/pe/README.md b/docs/pe/README.md deleted file mode 100644 index efe1231ab68..00000000000 --- a/docs/pe/README.md +++ /dev/null @@ -1,251 +0,0 @@ -
-Reflex Logo -
- -### **✨ برنامه های تحت وب قابل تنظیم، کارآمد تماما پایتونی که در چند ثانیه مستقر(دپلوی) می‎شود. ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) - -
- ---- - -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - رفلکس - -رفلکس(Reflex) یک کتابخانه برای ساخت برنامه های وب فول استک تماما پایتونی است. - -ویژگی های کلیدی: - -- **تماما پایتونی** - فرانت اند و بک اند برنامه خود را همه در پایتون بنویسید، بدون نیاز به یادگیری جاوا اسکریپت. -- **انعطاف پذیری کامل** - شروع به کار با Reflex آسان است، اما می تواند به برنامه های پیچیده نیز تبدیل شود. -- **دپلوی فوری** - پس از ساخت، برنامه خود را با [یک دستور](https://reflex.dev/docs/hosting/deploy-quick-start/) دپلوی کنید یا آن را روی سرور خود میزبانی کنید. - -برای آشنایی با نحوه عملکرد Reflex [صفحه معماری](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) را ببینید. - -## ⚙️ Installation - نصب و راه اندازی - -یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.10+): - -```bash -pip install reflex -``` - -## 🥳 اولین برنامه خود را ایجاد کنید - -نصب `reflex` همچنین `reflex` در خط فرمان را نصب میکند. - -با ایجاد یک پروژه جدید موفقیت آمیز بودن نصب را تست کنید. (`my_app_name` را با اسم پروژه خودتان جایگزین کنید): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -این دستور یک برنامه الگو(تمپلیت) را در فهرست(دایرکتوری) جدید شما مقداردهی اولیه می کند - -می توانید این برنامه را در حالت توسعه(development) اجرا کنید: - -```bash -reflex run -``` - -باید برنامه خود را در حال اجرا ببینید در http://localhost:3000. - -اکنون می‌توانید کد منبع را در «my_app_name/my_app_name.py» تغییر دهید. Reflex دارای تازه‌سازی‌های سریعی است، بنابراین می‌توانید تغییرات خود را بلافاصله پس از ذخیره کد خود مشاهده کنید. - -## 🫧 Example App - برنامه نمونه - -بیایید یک مثال بزنیم: ایجاد یک رابط کاربری برای تولید تصویر [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). برای سادگی، ما فراخوانی میکنیم [OpenAI API](https://platform.openai.com/docs/api-reference/authentication), اما می توانید آن را با یک مدل ML که به صورت محلی اجرا می شود جایگزین کنید. - -  - -
-A frontend wrapper for DALL·E, shown in the process of generating an image. -
- -  - -در اینجا کد کامل برای ایجاد این پروژه آمده است. همه اینها در یک فایل پایتون انجام می شود! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """The app state.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True - - -def index(): - return rx.center( - rx.vstack( - rx.heading("DALL-E", font_size="1.5em"), - rx.input( - placeholder="Enter a prompt..", - on_blur=State.set_prompt, - width="25em", - ), - rx.button( - "Generate Image", - on_click=State.get_image, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Add state and page to the app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## بیاید سادش کنیم - -
-Explaining the differences between backend and frontend parts of the DALL-E app. -
- -### **Reflex UI - رابط کاربری رفلکس** - -بیایید با رابط کاربری شروع کنیم. - -```python -def index(): - return rx.center( - ... - ) -``` - -تابع `index` قسمت فرانت اند برنامه را تعریف می کند. - -ما از اجزای مختلفی مثل `center`, `vstack`, `input` و `button` استفاده میکنیم تا فرانت اند را بسازیم. اجزاء را می توان درون یکدیگر قرار داد -برای ایجاد طرح بندی های پیچیده می توانید از args کلمات کلیدی برای استایل دادن به آنها از CSS استفاده کنید. - -رفلکس دارای [بیش از 60 جزء](https://reflex.dev/docs/library) برای کمک به شما برای شروع. ما به طور فعال اجزای بیشتری را اضافه می کنیم, و این خیلی آسان است که [اجزا خود را بسازید](https://reflex.dev/docs/wrapping-react/overview/). - -### **State - حالت** - -رفلکس رابط کاربری شما را به عنوان تابعی از وضعیت شما نشان می دهد. - -```python -class State(rx.State): - """The app state.""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -حالت تمام متغیرها(variables) (به نام vars) را در یک برنامه که می توانند تغییر دهند و توابعی که آنها را تغییر می دهند تعریف می کند.. - -در اینجا حالت از یک `prompt` و `image_url` تشکیل شده است. همچنین دو بولی `processing` و `complete` برای نشان دادن زمان غیرفعال کردن دکمه (در طول تولید تصویر) و زمان نمایش تصویر نتیجه وجود دارد. - -### **Event Handlers - کنترل کنندگان رویداد** - -```python -def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True -``` - -در داخل حالت، توابعی به نام کنترل کننده رویداد تعریف می کنیم که متغیرهای حالت را تغییر می دهند. کنترل کننده های رویداد راهی هستند که می توانیم وضعیت را در Reflex تغییر دهیم. آنها را می توان در پاسخ به اقدامات کاربر، مانند کلیک کردن روی یک دکمه یا تایپ کردن در یک متن، فراخوانی کرد. به این اعمال وقایع می گویند. - -برنامه DALL·E ما دارای یک کنترل کننده رویداد، `get_image` است که این تصویر را از OpenAI API دریافت می کند. استفاده از `yield` در وسط کنترل‌کننده رویداد باعث به‌روزرسانی رابط کاربری می‌شود. در غیر این صورت رابط کاربری در پایان کنترل کننده رویداد به روز می شود. - -### **Routing - مسیریابی** - -بالاخره اپلیکیشن خود را تعریف می کنیم. - -```python -app = rx.App() -``` - -ما یک صفحه از root برنامه را به جزء index اضافه می کنیم. ما همچنین عنوانی را اضافه می کنیم که در برگه پیش نمایش/مرورگر صفحه نمایش داده می شود. - -```python -app.add_page(index, title="DALL-E") -``` - -با افزودن صفحات بیشتر می توانید یک برنامه چند صفحه ای ایجاد کنید. - -## 📑 Resources - منابع - -
- -📑 [اسناد](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [وبلاگ](https://reflex.dev/blog)   |   📱 [کتابخانه جزء](https://reflex.dev/docs/library)   |   🖼️ [قالب ها](https://reflex.dev/templates/)   |   🛸 [استقرار](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Status - وضعیت - -رفلکس(reflex) در دسامبر 2022 با نام Pynecone راه اندازی شد. - -از سال 2025، [Reflex Cloud](https://cloud.reflex.dev) برای فراهم کردن بهترین تجربه میزبانی برای برنامه های Reflex راه‌اندازی شده است. ما به توسعه آن ادامه خواهیم داد و ویژگی‌های بیشتری را پیاده‌سازی خواهیم کرد. - -رفلکس(reflex) هر هفته نسخه ها و ویژگی های جدیدی دارد! مطمئن شوید که :star: ستاره و :eyes: این مخزن را تماشا کنید تا به روز بمانید. - -## Contributing - مشارکت کردن - -ما از مشارکت در هر اندازه استقبال می کنیم! در زیر چند راه خوب برای شروع در انجمن رفلکس آورده شده است. - -- **به Discord ما بپیوندید**: [Discord](https://discord.gg/T5WSbC2YtQ) ما بهترین مکان برای دریافت کمک در مورد پروژه Reflex و بحث در مورد اینکه چگونه می توانید کمک کنید است. -- **بحث های GitHub**: راهی عالی برای صحبت در مورد ویژگی هایی که می خواهید اضافه کنید یا چیزهایی که گیج کننده هستند/نیاز به توضیح دارند. -- **قسمت مشکلات GitHub**: [قسمت مشکلات](https://github.com/reflex-dev/reflex/issues) یک راه عالی برای گزارش اشکال هستند. علاوه بر این، می توانید یک مشکل موجود را حل کنید و یک PR(pull request) ارسال کنید. - -ما فعالانه به دنبال مشارکت کنندگان هستیم، فارغ از سطح مهارت یا تجربه شما. برای مشارکت [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) را بررسی کنید. - -## All Thanks To Our Contributors - با تشکر از همکاران ما: - - - - - -## License - مجوز - -رفلکس متن باز و تحت مجوز [Apache License 2.0](/LICENSE) است. diff --git a/docs/pt/pt_br/README.md b/docs/pt/pt_br/README.md deleted file mode 100644 index 1c970df9884..00000000000 --- a/docs/pt/pt_br/README.md +++ /dev/null @@ -1,251 +0,0 @@ -
-Reflex Logo -
- -### **✨ Web apps customizáveis, performáticos, em Python puro. Faça deploy em segundos. ✨** - -[![Versão PyPI](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versões](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentação](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) - -
- ---- - -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex é uma biblioteca para construir aplicações web full-stack em Python puro. - -Principais características: - -- **Python Puro** - Escreva o frontend e o backend da sua aplicação inteiramente em Python, sem necessidade de aprender Javascript. -- **Flexibilidade Total** - O Reflex é fácil de começar a usar, mas também pode escalar para aplicações complexas. -- **Deploy Instantâneo** - Após a construção, faça o deploy da sua aplicação com um [único comando](https://reflex.dev/docs/hosting/deploy-quick-start/) ou hospede-a em seu próprio servidor. - -Veja nossa [página de arquitetura](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) para aprender como o Reflex funciona internamente. - -## ⚙️ Instalação - -Abra um terminal e execute (Requer Python 3.10+): - -```bash -pip install reflex -``` - -## 🥳 Crie o seu primeiro app - -Instalar `reflex` também instala a ferramenta de linha de comando `reflex`. - -Crie um novo projeto para verificar se a instalação foi bem sucedida. (Mude `nome_do_meu_app` com o nome do seu projeto): - -```bash -mkdir nome_do_meu_app -cd nome_do_meu_app -reflex init -``` - -Este comando inicializa um app base no seu novo diretório. - -Você pode executar este app em modo desenvolvimento: - -```bash -reflex run -``` - -Você deve conseguir verificar seu app sendo executado em http://localhost:3000. - -Agora, você pode modificar o código fonte em `nome_do_meu_app/nome_do_meu_app.py`. O Reflex apresenta recarregamento rápido para que você possa ver suas mudanças instantaneamente quando você salva o seu código. - -## 🫧 Exemplo de App - -Veja o seguinte exemplo: criar uma interface de criação de imagens por meio do [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). Para fins de simplicidade, vamos apenas chamar a [API da OpenAI](https://platform.openai.com/docs/api-reference/authentication), mas você pode substituir esta solução por um modelo de ML executado localmente. - -  - -
-Um encapsulador frontend para o DALL-E, mostrado no processo de criação de uma imagem. -
- -  - -Aqui está o código completo para criar este projeto. Isso tudo foi feito apenas em um arquivo Python! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """Estado da aplicação.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Obtenção da imagem a partir do prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True - - -def index(): - return rx.center( - rx.vstack( - rx.heading("DALL-E", font_size="1.5em"), - rx.input( - placeholder="Enter a prompt..", - on_blur=State.set_prompt, - width="25em", - ), - rx.button( - "Generate Image", - on_click=State.get_image, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Adição do estado e da página no app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## Vamos por partes. - -
-Explicando as diferenças entre as partes de backend e frontend do app DALL-E. -
- -### **Reflex UI** - -Vamos começar com a UI (Interface de Usuário) - -```python -def index(): - return rx.center( - ... - ) -``` - -Esta função `index` define o frontend do app. - -Usamos diferentes componentes, como `center`, `vstack`, `input` e `button`, para construir o frontend. Componentes podem ser aninhados um no do outro -para criar layouts mais complexos. E você pode usar argumentos de chave-valor para estilizá-los com todo o poder do CSS. - -O Reflex vem com [60+ componentes nativos](https://reflex.dev/docs/library) para te ajudar a começar. Estamos adicionando ativamente mais componentes, e é fácil [criar seus próprios componentes](https://reflex.dev/docs/wrapping-react/overview/). - -### **Estado** - -O Reflex representa a sua UI como uma função do seu estado. - -```python -class State(rx.State): - """Estado da aplicação.""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -O estado define todas as variáveis (chamadas de vars) em um app que podem mudar e as funções que as alteram. - -Aqui, o estado é composto por um `prompt` e uma `image_url`. Há também os booleanos `processing` e `complete` para indicar quando desabilitar o botão (durante a geração da imagem) e quando mostrar a imagem resultante. - -### **Handlers de Eventos** - -```python -def get_image(self): - """Obtenção da imagem a partir do prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True -``` - -Dentro do estado, são definidas funções chamadas de Handlers de Eventos, que podem mudar as variáveis do estado. Handlers de Eventos são a forma com a qual podemos modificar o estado dentro do Reflex. Eles podem ser chamados como resposta a uma ação do usuário, como o clique de um botão ou a escrita em uma caixa de texto. Estas ações são chamadas de eventos. - -Nosso app DALL-E possui um Handler de Evento chamado `get_image`, que obtêm a imagem da API da OpenAI. Usar `yield` no meio de um Handler de Evento causa a atualização da UI do seu app. Caso contrário, a UI seria atualizada no fim da execução de um Handler de Evento. - -### **Rotas** - -Finalmente, definimos nosso app. - -```python -app = rx.App() -``` - -Adicionamos uma página na raíz do app, apontando para o componente index. Também adicionamos um título que irá aparecer na visualização da página/aba do navegador. - -```python -app.add_page(index, title="DALL-E") -``` - -Você pode criar um app com múltiplas páginas adicionando mais páginas. - -## 📑 Recursos - -
- -📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Biblioteca de Componentes](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Status - -O Reflex foi lançado em Dezembro de 2022 com o nome Pynecone. - -A partir de 2025, o [Reflex Cloud](https://cloud.reflex.dev) foi lançado para fornecer a melhor experiência de hospedagem para apps Reflex. Continuaremos a desenvolvê-lo e implementar mais recursos. - -O Reflex tem novas versões e recursos chegando toda semana! Certifique-se de marcar com :star: estrela e :eyes: observar este repositório para se manter atualizado. - -## Contribuições - -Nós somos abertos a contribuições de qualquer tamanho! Abaixo, seguem algumas boas formas de começar a contribuir para a comunidade do Reflex. - -- **Entre no nosso Discord**: Nosso [Discord](https://discord.gg/T5WSbC2YtQ) é o melhor lugar para conseguir ajuda no seu projeto Reflex e para discutir como você pode contribuir. -- **Discussões no GitHub**: Uma boa forma de conversar sobre funcionalidades que você gostaria de ver ou coisas que ainda estão confusas/exigem ajuda. -- **Issues no GitHub**: [Issues](https://github.com/reflex-dev/reflex/issues) são uma excelente forma de reportar bugs. Além disso, você pode tentar resolver alguma issue existente e enviar um PR. - -Estamos ativamente buscando novos contribuidores, não importa o seu nível de habilidade ou experiência. Para contribuir, confira [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md). - -## Todo Agradecimento aos Nossos Contribuidores: - - - - - -## Licença - -O Reflex é de código aberto e licenciado sob a [Apache License 2.0](/LICENSE). diff --git a/docs/recipes/auth/login_form.md b/docs/recipes/auth/login_form.md new file mode 100644 index 00000000000..9189800e4ad --- /dev/null +++ b/docs/recipes/auth/login_form.md @@ -0,0 +1,243 @@ +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +``` + +# Login Form + +The login form is a common component in web applications. It allows users to authenticate themselves and access their accounts. This recipe provides examples of login forms with different elements, such as third-party authentication providers. + +## Default + +```python demo exec toggle +def login_default() -> rx.Component: + return rx.card( + rx.vstack( + rx.center( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Sign in to your account", size="6", as_="h2", text_align="center", width="100%"), + direction="column", + spacing="5", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(placeholder="user@reflex.dev", type="email", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.vstack( + rx.hstack( + rx.text("Password", size="3", weight="medium"), + rx.link("Forgot password?", href="#", size="3"), + justify="between", + width="100%" + ), + rx.input(placeholder="Enter your password", type="password", size="3", width="100%"), + spacing="2", + width="100%" + ), + rx.button("Sign in", size="3", width="100%"), + rx.center( + rx.text("New here?", size="3"), + rx.link("Sign up", href="#", size="3"), + opacity="0.8", + spacing="2", + direction="row" + ), + spacing="6", + width="100%" + ), + size="4", + max_width="28em", + width="100%" + ) +``` + +## Icons + +```python demo exec toggle +def login_default_icons() -> rx.Component: + return rx.card( + rx.vstack( + rx.center( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Sign in to your account", size="6", as_="h2", text_align="center", width="100%"), + direction="column", + spacing="5", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("user")), placeholder="user@reflex.dev", type="email", size="3", width="100%"), + spacing="2", + width="100%" + ), + rx.vstack( + rx.hstack( + rx.text("Password", size="3", weight="medium"), + rx.link("Forgot password?", href="#", size="3"), + justify="between", + width="100%" + ), + rx.input(rx.input.slot(rx.icon("lock")), placeholder="Enter your password", type="password", size="3", width="100%"), + spacing="2", + width="100%" + ), + rx.button("Sign in", size="3", width="100%"), + rx.center( + rx.text("New here?", size="3"), + rx.link("Sign up", href="#", size="3"), + opacity="0.8", + spacing="2", + direction="row", + width="100%" + ), + spacing="6", + width="100%" + ), + max_width="28em", + size="4", + width="100%" + ) +``` + +## Third-party auth + +```python demo exec toggle +def login_single_thirdparty() -> rx.Component: + return rx.card( + rx.vstack( + rx.flex( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Sign in to your account", size="6", as_="h2", text_align="left", width="100%"), + rx.hstack( + rx.text("New here?", size="3", text_align="left"), + rx.link("Sign up", href="#", size="3"), + spacing="2", + opacity="0.8", + width="100%" + ), + direction="column", + justify="start", + spacing="4", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("user")), placeholder="user@reflex.dev", type="email", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.vstack( + rx.hstack( + rx.text("Password", size="3", weight="medium"), + rx.link("Forgot password?", href="#", size="3"), + justify="between", + width="100%" + ), + rx.input(rx.input.slot(rx.icon("lock")), placeholder="Enter your password", type="password", size="3", width="100%"), + spacing="2", + width="100%" + ), + rx.button("Sign in", size="3", width="100%"), + rx.hstack( + rx.divider(margin="0"), + rx.text("Or continue with", white_space="nowrap", weight="medium"), + rx.divider(margin="0"), + align="center", + width="100%" + ), + rx.button( + rx.icon(tag="github"), + "Sign in with Github", + variant="outline", + size="3", + width="100%" + ), + spacing="6", + width="100%" + ), + size="4", + max_width="28em", + width="100%" + ) +``` + +## Multiple third-party auth + +```python demo exec toggle +def login_multiple_thirdparty() -> rx.Component: + return rx.card( + rx.vstack( + rx.flex( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Sign in to your account", size="6", as_="h2", width="100%"), + rx.hstack( + rx.text("New here?", size="3", text_align="left"), + rx.link("Sign up", href="#", size="3"), + spacing="2", + opacity="0.8", + width="100%" + ), + justify="start", + direction="column", + spacing="4", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("user")), placeholder="user@reflex.dev", type="email", size="3", width="100%"), + spacing="2", + justify="start", + width="100%" + ), + rx.vstack( + rx.hstack( + rx.text("Password", size="3", weight="medium"), + rx.link("Forgot password?", href="#", size="3"), + justify="between", + width="100%" + ), + rx.input(rx.input.slot(rx.icon("lock")), placeholder="Enter your password", type="password", size="3", width="100%"), + spacing="2", + width="100%" + ), + rx.button("Sign in", size="3", width="100%"), + rx.hstack( + rx.divider(margin="0"), + rx.text("Or continue with", white_space="nowrap", weight="medium"), + rx.divider(margin="0"), + align="center", + width="100%" + ), + rx.center( + rx.icon_button( + rx.icon(tag="github"), + variant="soft", + size="3" + ), + rx.icon_button( + rx.icon(tag="facebook"), + variant="soft", + size="3" + ), + rx.icon_button( + rx.icon(tag="twitter"), + variant="soft", + size="3" + ), + spacing="4", + direction="row", + width="100%" + ), + spacing="6", + width="100%" + ), + size="4", + max_width="28em", + width="100%" + ) +``` diff --git a/docs/recipes/auth/signup_form.md b/docs/recipes/auth/signup_form.md new file mode 100644 index 00000000000..90f8fb3c0f0 --- /dev/null +++ b/docs/recipes/auth/signup_form.md @@ -0,0 +1,260 @@ +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +``` + +# Sign up Form + +The sign up form is a common component in web applications. It allows users to create an account and access the application's features. This page provides a few examples of sign up forms that you can use in your application. + +## Default + +```python demo exec toggle +def signup_default() -> rx.Component: + return rx.card( + rx.vstack( + rx.center( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Create an account", size="6", as_="h2", text_align="center", width="100%"), + direction="column", + spacing="5", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(placeholder="user@reflex.dev", type="email", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.vstack( + rx.text("Password", size="3", weight="medium", text_align="left", width="100%"), + rx.input(placeholder="Enter your password", type="password", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.box( + rx.checkbox( + "Agree to Terms and Conditions", + default_checked=True, + spacing="2" + ), + width="100%" + ), + rx.button("Register", size="3", width="100%"), + rx.center( + rx.text("Already registered?", size="3"), + rx.link("Sign in", href="#", size="3"), + opacity="0.8", + spacing="2", + direction="row" + ), + spacing="6", + width="100%" + ), + size="4", + max_width="28em", + width="100%" + ) +``` + +## Icons + +```python demo exec toggle +def signup_default_icons() -> rx.Component: + return rx.card( + rx.vstack( + rx.center( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Create an account", size="6", as_="h2", text_align="center", width="100%"), + direction="column", + spacing="5", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("user")), placeholder="user@reflex.dev", type="email", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.vstack( + rx.text("Password", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("lock")), placeholder="Enter your password", type="password", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.box( + rx.checkbox( + "Agree to Terms and Conditions", + default_checked=True, + spacing="2" + ), + width="100%" + ), + rx.button("Register", size="3", width="100%"), + rx.center( + rx.text("Already registered?", size="3"), + rx.link("Sign in", href="#", size="3"), + opacity="0.8", + spacing="2", + direction="row", + width="100%" + ), + spacing="6", + width="100%" + ), + max_width="28em", + size="4", + width="100%" + ) +``` + +## Third-party auth + +```python demo exec toggle +def signup_single_thirdparty() -> rx.Component: + return rx.card( + rx.vstack( + rx.flex( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Create an account", size="6", as_="h2", text_align="left", width="100%"), + rx.hstack( + rx.text("Already registered?", size="3", text_align="left"), + rx.link("Sign in", href="#", size="3"), + spacing="2", + opacity="0.8", + width="100%" + ), + direction="column", + justify="start", + spacing="4", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("user")), placeholder="user@reflex.dev", type="email", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.vstack( + rx.text("Password", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("lock")), placeholder="Enter your password", type="password", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.box( + rx.checkbox( + "Agree to Terms and Conditions", + default_checked=True, + spacing="2" + ), + width="100%" + ), + rx.button("Register", size="3", width="100%"), + rx.hstack( + rx.divider(margin="0"), + rx.text("Or continue with", white_space="nowrap", weight="medium"), + rx.divider(margin="0"), + align="center", + width="100%" + ), + rx.button( + rx.icon(tag="github"), + "Sign in with Github", + variant="outline", + size="3", + width="100%" + ), + spacing="6", + width="100%" + ), + size="4", + max_width="28em", + width="100%" + ) +``` + +## Multiple third-party auth + +```python demo exec toggle +def signup_multiple_thirdparty() -> rx.Component: + return rx.card( + rx.vstack( + rx.flex( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.5em", height="auto", border_radius="25%"), + rx.heading("Create an account", size="6", as_="h2", width="100%"), + rx.hstack( + rx.text("Already registered?", size="3", text_align="left"), + rx.link("Sign in", href="#", size="3"), + spacing="2", + opacity="0.8", + width="100%" + ), + justify="start", + direction="column", + spacing="4", + width="100%" + ), + rx.vstack( + rx.text("Email address", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("user")), placeholder="user@reflex.dev", type="email", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.vstack( + rx.text("Password", size="3", weight="medium", text_align="left", width="100%"), + rx.input(rx.input.slot(rx.icon("lock")), placeholder="Enter your password", type="password", size="3", width="100%"), + justify="start", + spacing="2", + width="100%" + ), + rx.box( + rx.checkbox( + "Agree to Terms and Conditions", + default_checked=True, + spacing="2" + ), + width="100%" + ), + rx.button("Register", size="3", width="100%"), + rx.hstack( + rx.divider(margin="0"), + rx.text("Or continue with", white_space="nowrap", weight="medium"), + rx.divider(margin="0"), + align="center", + width="100%" + ), + rx.center( + rx.icon_button( + rx.icon(tag="github"), + variant="soft", + size="3" + ), + rx.icon_button( + rx.icon(tag="facebook"), + variant="soft", + size="3" + ), + rx.icon_button( + rx.icon(tag="twitter"), + variant="soft", + size="3" + ), + spacing="4", + direction="row", + width="100%" + ), + spacing="6", + width="100%" + ), + size="4", + max_width="28em", + width="100%" + ) +``` diff --git a/docs/recipes/content/forms.md b/docs/recipes/content/forms.md new file mode 100644 index 00000000000..0b4a076af0f --- /dev/null +++ b/docs/recipes/content/forms.md @@ -0,0 +1,173 @@ +```python exec +import reflex as rx +from pcweb.pages.docs import library +``` + +## Forms + +Forms are a common way to gather information from users. Below are some examples. + +For more details, see the [form docs page]({library.forms.form.path}). + +## Event creation + +```python demo exec toggle +def form_field(label: str, placeholder: str, type: str, name: str) -> rx.Component: + return rx.form.field( + rx.flex( + rx.form.label(label), + rx.form.control( + rx.input( + placeholder=placeholder, + type=type + ), + as_child=True, + ), + direction="column", + spacing="1", + ), + name=name, + width="100%" + ) + +def event_form() -> rx.Component: + return rx.card( + rx.flex( + rx.hstack( + rx.badge( + rx.icon(tag="calendar-plus", size=32), + color_scheme="mint", + radius="full", + padding="0.65rem" + ), + rx.vstack( + rx.heading("Create an event", size="4", weight="bold"), + rx.text("Fill the form to create a custom event", size="2"), + spacing="1", + height="100%", + align_items="start" + ), + height="100%", + spacing="4", + align_items="center", + width="100%", + ), + rx.form.root( + rx.flex( + form_field("Event Name", "Event Name", + "text", "event_name"), + rx.flex( + form_field("Date", "", "date", "event_date"), + form_field("Time", "", "time", "event_time"), + spacing="3", + flex_direction="row", + ), + form_field("Description", "Optional", "text", "description"), + direction="column", + spacing="2", + ), + rx.form.submit( + rx.button("Create"), + as_child=True, + width="100%", + ), + on_submit=lambda form_data: rx.window_alert(form_data.to_string()), + reset_on_submit=False, + ), + width="100%", + direction="column", + spacing="4", + ), + size="3", + ) +``` + +## Contact + +```python demo exec toggle +def form_field(label: str, placeholder: str, type: str, name: str) -> rx.Component: + return rx.form.field( + rx.flex( + rx.form.label(label), + rx.form.control( + rx.input( + placeholder=placeholder, + type=type + ), + as_child=True, + ), + direction="column", + spacing="1", + ), + name=name, + width="100%" + ) + +def contact_form() -> rx.Component: + return rx.card( + rx.flex( + rx.hstack( + rx.badge( + rx.icon(tag="mail-plus", size=32), + color_scheme="blue", + radius="full", + padding="0.65rem" + ), + rx.vstack( + rx.heading("Send us a message", size="4", weight="bold"), + rx.text("Fill the form to contact us", size="2"), + spacing="1", + height="100%", + ), + height="100%", + spacing="4", + align_items="center", + width="100%", + ), + rx.form.root( + rx.flex( + rx.flex( + form_field("First Name", "First Name", + "text", "first_name"), + form_field("Last Name", "Last Name", + "text", "last_name"), + spacing="3", + flex_direction=["column", "row", "row"], + ), + rx.flex( + form_field("Email", "user@reflex.dev", + "email", "email"), + form_field("Phone", "Phone", "tel", "phone"), + spacing="3", + flex_direction=["column", "row", "row"], + ), + rx.flex( + rx.text("Message", style={ + "font-size": "15px", "font-weight": "500", "line-height": "35px"}), + rx.text_area( + placeholder="Message", + name="message", + resize="vertical", + ), + direction="column", + spacing="1", + ), + rx.form.submit( + rx.button("Submit"), + as_child=True, + ), + direction="column", + spacing="2", + width="100%", + ), + on_submit=lambda form_data: rx.window_alert( + form_data.to_string()), + reset_on_submit=False, + ), + width="100%", + direction="column", + spacing="4", + ), + size="3", + ) +``` diff --git a/docs/recipes/content/grid.md b/docs/recipes/content/grid.md new file mode 100644 index 00000000000..5c629c5f65a --- /dev/null +++ b/docs/recipes/content/grid.md @@ -0,0 +1,52 @@ +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +from pcweb.pages.docs import styling +``` + +# Grid + +A simple responsive grid layout. We specify the number of columns to the `grid_template_columns` property as a list. The grid will automatically adjust the number of columns based on the screen size. + +For details, see the [responsive docs page]({styling.responsive.path}). + +## Cards + +```python demo +rx.grid( + rx.foreach( + rx.Var.range(12), + lambda i: rx.card(f"Card {i + 1}", height="10vh"), + ), + gap="1rem", + grid_template_columns=["1fr", "repeat(2, 1fr)", "repeat(2, 1fr)", "repeat(3, 1fr)", "repeat(4, 1fr)"], + width="100%" +) +``` + +## Inset cards + +```python demo +rx.grid( + rx.foreach( + rx.Var.range(12), + lambda i: rx.card( + rx.inset( + rx.image( + src=f"{REFLEX_ASSETS_CDN}other/reflex_banner.png", + width="100%", + height="auto", + ), + side="top", + pb="current" + ), + rx.text( + f"Card {i + 1}", + ), + ), + ), + gap="1rem", + grid_template_columns=["1fr", "repeat(2, 1fr)", "repeat(2, 1fr)", "repeat(3, 1fr)", "repeat(4, 1fr)"], + width="100%" +) +``` diff --git a/docs/recipes/content/multi_column_row.md b/docs/recipes/content/multi_column_row.md new file mode 100644 index 00000000000..891da8cc606 --- /dev/null +++ b/docs/recipes/content/multi_column_row.md @@ -0,0 +1,64 @@ +```python exec +import reflex as rx +from pcweb.pages.docs import styling +``` + +# Multi-column and row layout + +A simple responsive multi-column and row layout. We specify the number of columns/rows to the `flex_direction` property as a list. The layout will automatically adjust the number of columns/rows based on the screen size. + +For details, see the [responsive docs page]({styling.responsive.path}). + +## Column + +```python demo +rx.flex( + rx.box(bg=rx.color("accent", 3), width="100%", height="100%"), + rx.box(bg=rx.color("accent", 5), width="100%", height="100%"), + rx.box(bg=rx.color("accent", 7), width="100%", height="100%"), + bg=rx.color("accent", 10), + spacing="4", + padding="1em", + flex_direction=["column", "column", "row"], + height="600px", + width="100%", +) +``` + +```python demo +rx.flex( + rx.box(bg=rx.color("accent", 3), width="100%", height="100%"), + rx.box(bg=rx.color("accent", 5), width=["100%", "100%", "50%"], height=["50%", "50%", "100%"]), + rx.box(bg=rx.color("accent", 7), width="100%", height="100%"), + rx.box(bg=rx.color("accent", 9), width=["100%", "100%", "50%"], height=["50%", "50%", "100%"]), + bg=rx.color("accent", 10), + spacing="4", + padding="1em", + flex_direction=["column", "column", "row"], + height="600px", + width="100%", +) +``` + +## Row + +```python demo +rx.flex( + rx.flex( + rx.box(bg=rx.color("accent", 3), width=["100%", "100%", "50%"], height="100%"), + rx.box(bg=rx.color("accent", 5), width=["100%", "100%", "50%"], height="100%"), + width="100%", + height="100%", + spacing="4", + flex_direction=["column", "column", "row"], + ), + rx.box(bg=rx.color("accent", 7), width="100%", height="50%"), + rx.box(bg=rx.color("accent", 9), width="100%", height="50%"), + bg=rx.color("accent", 10), + spacing="4", + padding="1em", + flex_direction="column", + height="600px", + width="100%", +) +``` diff --git a/docs/recipes/content/stats.md b/docs/recipes/content/stats.md new file mode 100644 index 00000000000..b4d038835fa --- /dev/null +++ b/docs/recipes/content/stats.md @@ -0,0 +1,107 @@ +```python exec +import reflex as rx +``` + +# Stats + +Stats cards are used to display key metrics or data points. They are typically used in dashboards or admin panels. + +## Variant 1 + +```python demo exec toggle +from reflex.components.radix.themes.base import LiteralAccentColor + +def stats(stat_name: str = "Users", value: int = 4200, prev_value: int = 3000, icon: str = "users", badge_color: LiteralAccentColor = "blue") -> rx.Component: + percentage_change = round(((value - prev_value) / prev_value) * 100, 2) if prev_value != 0 else 0 if value == 0 else float('inf') + change = "increase" if value > prev_value else "decrease" + arrow_icon = "trending-up" if value > prev_value else "trending-down" + arrow_color = "grass" if value > prev_value else "tomato" + return rx.card( + rx.vstack( + rx.hstack( + rx.badge( + rx.icon(tag=icon, size=34), + color_scheme=badge_color, + radius="full", + padding="0.7rem" + ), + rx.vstack( + rx.heading(f"{value:,}", size="6", weight="bold"), + rx.text(stat_name, size="4", weight="medium"), + spacing="1", + height="100%", + align_items="start", + width="100%" + ), + height="100%", + spacing="4", + align="center", + width="100%", + ), + rx.hstack( + rx.hstack( + rx.icon(tag=arrow_icon, size=24, color=rx.color(arrow_color, 9)), + rx.text(f"{percentage_change}%", size="3", color=rx.color(arrow_color, 9), weight="medium"), + spacing="2", + align="center", + ), + rx.text(f"{change} from last month", size="2", color=rx.color("gray", 10)), + align="center", + width="100%", + ), + spacing="3", + ), + size="3", + width="100%", + max_width="21rem" + ) +``` + +## Variant 2 + +```python demo exec toggle +from reflex.components.radix.themes.base import LiteralAccentColor + +def stats_2(stat_name: str = "Orders", value: int = 6500, prev_value: int = 12000, icon: str = "shopping-cart", icon_color: LiteralAccentColor = "pink") -> rx.Component: + percentage_change = round(((value - prev_value) / prev_value) * 100, 2) if prev_value != 0 else 0 if value == 0 else float('inf') + arrow_icon = "trending-up" if value > prev_value else "trending-down" + arrow_color = "grass" if value > prev_value else "tomato" + return rx.card( + rx.hstack( + rx.vstack( + rx.hstack( + rx.hstack( + rx.icon(tag=icon, size=22, color=rx.color(icon_color, 11)), + rx.text(stat_name, size="4", weight="medium", color=rx.color("gray", 11)), + spacing="2", + align="center", + ), + rx.badge( + rx.icon(tag=arrow_icon, color=rx.color(arrow_color, 9)), + rx.text(f"{percentage_change}%", size="2", color=rx.color(arrow_color, 9), weight="medium"), + color_scheme=arrow_color, + radius="large", + align_items="center", + ), + justify="between", + width="100%", + ), + rx.hstack( + rx.heading(f"{value:,}", size="7", weight="bold"), + rx.text(f"from {prev_value:,}", size="3", color=rx.color("gray", 10)), + spacing="2", + align_items="end", + ), + align_items="start", + justify="between", + width="100%", + ), + align_items="start", + width="100%", + justify="between", + ), + size="3", + width="100%", + max_width="21rem", + ) +``` diff --git a/docs/recipes/content/top_banner.md b/docs/recipes/content/top_banner.md new file mode 100644 index 00000000000..b4e2c0b2855 --- /dev/null +++ b/docs/recipes/content/top_banner.md @@ -0,0 +1,271 @@ +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +``` + +# Top Banner + +Top banners are used to highlight important information or features at the top of a page. They are typically designed to grab the user's attention and can be used for announcements, navigation, or key messages. + +## Basic + +```python demo exec toggle +class TopBannerBasic(rx.ComponentState): + hide: bool = False + + @rx.event + def toggle(self): + self.hide = not self.hide + + @classmethod + def get_component(cls, **props): + return rx.cond( + ~cls.hide, + rx.hstack( + rx.flex( + rx.badge( + rx.icon("megaphone", size=18), + padding="0.30rem", + radius="full", + ), + rx.text( + "ReflexCon 2024 - ", + rx.link( + "Join us at the event!", + href="#", + underline="always", + display="inline", + underline_offset="2px", + ), + weight="medium", + ), + align="center", + margin="auto", + spacing="3", + ), + rx.icon( + "x", + cursor="pointer", + justify="end", + flex_shrink=0, + on_click=cls.toggle, + ), + wrap="nowrap", + # position="fixed", + justify="between", + width="100%", + # top="0", + align="center", + left="0", + # z_index="50", + padding="1rem", + background=rx.color("accent", 4), + **props, + ), + # Remove this in production + rx.icon_button( + rx.icon("eye"), + cursor="pointer", + on_click=cls.toggle, + ), + ) + +top_banner_basic = TopBannerBasic.create +``` + +## Sign up + +```python demo exec toggle +class TopBannerSignup(rx.ComponentState): + hide: bool = False + + @rx.event + def toggle(self): + self.hide = not self.hide + + @classmethod + def get_component(cls, **props): + return rx.cond( + ~cls.hide, + rx.flex( + rx.image( + src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", + width="2em", + height="auto", + border_radius="25%", + ), + rx.text( + "Web apps in pure Python. Deploy with a single command.", + weight="medium", + ), + rx.flex( + rx.button( + "Sign up", + cursor="pointer", + radius="large", + ), + rx.icon( + "x", + cursor="pointer", + justify="end", + flex_shrink=0, + on_click=cls.toggle, + ), + spacing="4", + align="center", + ), + wrap="nowrap", + # position="fixed", + flex_direction=["column", "column", "row"], + justify_content=["start", "space-between"], + width="100%", + # top="0", + spacing="2", + align_items=["start", "start", "center"], + left="0", + # z_index="50", + padding="1rem", + background=rx.color("accent", 4), + **props, + ), + # Remove this in production + rx.icon_button( + rx.icon("eye"), + cursor="pointer", + on_click=cls.toggle, + ), + ) + +top_banner_signup = TopBannerSignup.create +``` + +## Gradient + +```python demo exec toggle +class TopBannerGradient(rx.ComponentState): + hide: bool = False + + @rx.event + def toggle(self): + self.hide = not self.hide + + @classmethod + def get_component(cls, **props): + return rx.cond( + ~cls.hide, + rx.flex( + rx.text( + "The new Reflex version is now available! ", + rx.link( + "Read the release notes", + href="#", + underline="always", + display="inline", + underline_offset="2px", + ), + align_items=["start", "center"], + margin="auto", + spacing="3", + weight="medium", + ), + rx.icon( + "x", + cursor="pointer", + justify="end", + flex_shrink=0, + on_click=cls.toggle, + ), + wrap="nowrap", + # position="fixed", + justify="between", + width="100%", + # top="0", + align="center", + left="0", + # z_index="50", + padding="1rem", + background=f"linear-gradient(99deg, {rx.color('blue', 4)}, {rx.color('pink', 3)}, {rx.color('mauve', 3)})", + **props, + ), + # Remove this in production + rx.icon_button( + rx.icon("eye"), + cursor="pointer", + on_click=cls.toggle, + ), + ) + +top_banner_gradient = TopBannerGradient.create +``` + +## Newsletter + +```python demo exec toggle +class TopBannerNewsletter(rx.ComponentState): + hide: bool = False + + @rx.event + def toggle(self): + self.hide = not self.hide + + @classmethod + def get_component(cls, **props): + return rx.cond( + ~cls.hide, + rx.flex( + rx.text( + "Join our newsletter", + text_wrap="nowrap", + weight="medium", + ), + rx.input( + rx.input.slot(rx.icon("mail")), + rx.input.slot( + rx.icon_button( + rx.icon( + "arrow-right", + padding="0.15em", + ), + cursor="pointer", + radius="large", + size="2", + justify="end", + ), + padding_right="0", + ), + placeholder="Your email address", + type="email", + size="2", + radius="large", + ), + rx.icon( + "x", + cursor="pointer", + justify="end", + flex_shrink=0, + on_click=cls.toggle, + ), + wrap="nowrap", + # position="fixed", + flex_direction=["column", "row", "row"], + justify_content=["start", "space-between"], + width="100%", + # top="0", + spacing="2", + align_items=["start", "center", "center"], + left="0", + # z_index="50", + padding="1rem", + background=rx.color("accent", 4), + **props, + ), + # Remove this in production + rx.icon_button( + rx.icon("eye"), + cursor="pointer", + on_click=cls.toggle, + ), + ) + +top_banner_newsletter = TopBannerNewsletter.create +``` diff --git a/docs/recipes/layout/footer.md b/docs/recipes/layout/footer.md new file mode 100644 index 00000000000..d13e12bae91 --- /dev/null +++ b/docs/recipes/layout/footer.md @@ -0,0 +1,281 @@ +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +``` + +# Footer Bar + +A footer bar is a common UI element located at the bottom of a webpage. It typically contains information about the website, such as contact details and links to other pages or sections of the site. + +## Basic + +```python demo exec toggle +def footer_item(text: str, href: str) -> rx.Component: + return rx.link(rx.text(text, size="3"), href=href) + +def footer_items_1() -> rx.Component: + return rx.flex( + rx.heading("PRODUCTS", size="4", weight="bold", as_="h3"), + footer_item("Web Design", "/#"), + footer_item("Web Development", "/#"), + footer_item("E-commerce", "/#"), + footer_item("Content Management", "/#"), + footer_item("Mobile Apps", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def footer_items_2() -> rx.Component: + return rx.flex( + rx.heading("RESOURCES", size="4", weight="bold", as_="h3"), + footer_item("Blog", "/#"), + footer_item("Case Studies", "/#"), + footer_item("Whitepapers", "/#"), + footer_item("Webinars", "/#"), + footer_item("E-books", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def social_link(icon: str, href: str) -> rx.Component: + return rx.link(rx.icon(icon), href=href) + +def socials() -> rx.Component: + return rx.flex( + social_link("instagram", "/#"), + social_link("twitter", "/#"), + social_link("facebook", "/#"), + social_link("linkedin", "/#"), + spacing="3", + justify="end", + width="100%" + ) + +def footer() -> rx.Component: + return rx.el.footer( + rx.vstack( + rx.flex( + rx.vstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), + align_items="center" + ), + rx.text("© 2024 Reflex, Inc", size="3", white_space="nowrap", weight="medium"), + spacing="4", + align_items=["center", "center", "start"] + ), + footer_items_1(), + footer_items_2(), + justify="between", + spacing="6", + flex_direction=["column", "column", "row"], + width="100%" + ), + rx.divider(), + rx.hstack( + rx.hstack( + footer_item("Privacy Policy", "/#"), + footer_item("Terms of Service", "/#"), + spacing="4", + align="center", + width="100%" + ), + socials(), + justify="between", + width="100%" + ), + spacing="5", + width="100%" + ), + width="100%" + ) +``` + +## Newsletter form + +```python demo exec toggle +def footer_item(text: str, href: str) -> rx.Component: + return rx.link(rx.text(text, size="3"), href=href) + +def footer_items_1() -> rx.Component: + return rx.flex( + rx.heading("PRODUCTS", size="4", weight="bold", as_="h3"), + footer_item("Web Design", "/#"), + footer_item("Web Development", "/#"), + footer_item("E-commerce", "/#"), + footer_item("Content Management", "/#"), + footer_item("Mobile Apps", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def footer_items_2() -> rx.Component: + return rx.flex( + rx.heading("RESOURCES", size="4", weight="bold", as_="h3"), + footer_item("Blog", "/#"), + footer_item("Case Studies", "/#"), + footer_item("Whitepapers", "/#"), + footer_item("Webinars", "/#"), + footer_item("E-books", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def social_link(icon: str, href: str) -> rx.Component: + return rx.link(rx.icon(icon), href=href) + +def socials() -> rx.Component: + return rx.flex( + social_link("instagram", "/#"), + social_link("twitter", "/#"), + social_link("facebook", "/#"), + social_link("linkedin", "/#"), + spacing="3", + justify_content=["center", "center", "end"], + width="100%" + ) + +def footer_newsletter() -> rx.Component: + return rx.el.footer( + rx.vstack( + rx.flex( + footer_items_1(), + footer_items_2(), + rx.vstack( + rx.text("JOIN OUR NEWSLETTER", size="4", + weight="bold"), + rx.hstack( + rx.input(placeholder="Your email address", type="email", size="3"), + rx.icon_button(rx.icon("arrow-right", padding="0.15em"), size="3"), + spacing="1", + justify="center", + width="100%" + ), + align_items=["center", "center", "start"], + justify="center", + height="100%" + ), + justify="between", + spacing="6", + flex_direction=["column", "column", "row"], + width="100%" + ), + rx.divider(), + rx.flex( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.text("© 2024 Reflex, Inc", size="3", white_space="nowrap", weight="medium"), + spacing="2", + align="center", + justify_content=["center", "center", "start"], + width="100%" + ), + socials(), + spacing="4", + flex_direction=["column", "column", "row"], + width="100%" + ), + spacing="5", + width="100%" + ), + width="100%" + ) +``` + +## Three columns + +```python demo exec toggle +def footer_item(text: str, href: str) -> rx.Component: + return rx.link(rx.text(text, size="3"), href=href) + +def footer_items_1() -> rx.Component: + return rx.flex( + rx.heading("PRODUCTS", size="4", weight="bold", as_="h3"), + footer_item("Web Design", "/#"), + footer_item("Web Development", "/#"), + footer_item("E-commerce", "/#"), + footer_item("Content Management", "/#"), + footer_item("Mobile Apps", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def footer_items_2() -> rx.Component: + return rx.flex( + rx.heading("RESOURCES", size="4", weight="bold", as_="h3"), + footer_item("Blog", "/#"), + footer_item("Case Studies", "/#"), + footer_item("Whitepapers", "/#"), + footer_item("Webinars", "/#"), + footer_item("E-books", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def footer_items_3() -> rx.Component: + return rx.flex( + rx.heading("ABOUT US", size="4", weight="bold", as_="h3"), + footer_item("Our Team", "/#"), + footer_item("Careers", "/#"), + footer_item("Contact Us", "/#"), + footer_item("Privacy Policy", "/#"), + footer_item("Terms of Service", "/#"), + spacing="4", + text_align=["center", "center", "start"], + flex_direction="column" + ) + +def social_link(icon: str, href: str) -> rx.Component: + return rx.link(rx.icon(icon), href=href) + +def socials() -> rx.Component: + return rx.flex( + social_link("instagram", "/#"), + social_link("twitter", "/#"), + social_link("facebook", "/#"), + social_link("linkedin", "/#"), + spacing="3", + justify_content=["center", "center", "end"], + width="100%" + ) + +def footer_three_columns() -> rx.Component: + return rx.el.footer( + rx.vstack( + rx.flex( + footer_items_1(), + footer_items_2(), + footer_items_3(), + justify="between", + spacing="6", + flex_direction=["column", "column", "row"], + width="100%" + ), + rx.divider(), + rx.flex( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.text("© 2024 Reflex, Inc", size="3", white_space="nowrap", weight="medium"), + spacing="2", + align="center", + justify_content=["center", "center", "start"], + width="100%" + ), + socials(), + spacing="4", + flex_direction=["column", "column", "row"], + width="100%" + ), + spacing="5", + width="100%" + ), + width="100%" + ) +``` diff --git a/docs/recipes/layout/navbar.md b/docs/recipes/layout/navbar.md new file mode 100644 index 00000000000..5ca3acde41a --- /dev/null +++ b/docs/recipes/layout/navbar.md @@ -0,0 +1,365 @@ +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +``` + +# Navigation Bar + +A navigation bar, also known as a navbar, is a common UI element found at the top of a webpage or application. +It typically provides links or buttons to the main sections of a website or application, allowing users to easily navigate and access the different pages. + +Navigation bars are useful for web apps because they provide a consistent and intuitive way for users to navigate through the app. +Having a clear and consistent navigation structure can greatly improve the user experience by making it easy for users to find the information they need and access the different features of the app. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=2365&end=2627 +# Video: Example of Using the Navbar Recipe +``` + +## Basic + +```python demo exec toggle +def navbar_link(text: str, url: str) -> rx.Component: + return rx.link(rx.text(text, size="4", weight="medium"), href=url) + +def navbar() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.hstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), align_items="center"), + rx.hstack( + navbar_link("Home", "/#"), + navbar_link("About", "/#"), + navbar_link("Pricing", "/#"), + navbar_link("Contact", "/#"), + justify="end", + spacing="5" + ), + justify="between", + align_items="center" + ), + ), + rx.mobile_and_tablet( + rx.hstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2em", + height="auto", border_radius="25%"), + rx.heading("Reflex", size="6", weight="bold"), align_items="center"), + rx.menu.root( + rx.menu.trigger(rx.icon("menu", size=30)), + rx.menu.content( + rx.menu.item("Home"), + rx.menu.item("About"), + rx.menu.item("Pricing"), + rx.menu.item("Contact"), + ), + justify="end" + ), + justify="between", + align_items="center" + ), + ), + bg=rx.color("accent", 3), + padding="1em", + # position="fixed", + # top="0px", + # z_index="5", + width="100%" + ) +``` + +## Dropdown + +```python demo exec toggle +def navbar_link(text: str, url: str) -> rx.Component: + return rx.link(rx.text(text, size="4", weight="medium"), href=url) + +def navbar_dropdown() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.hstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), align_items="center"), + rx.hstack( + navbar_link("Home", "/#"), + rx.menu.root( + rx.menu.trigger( + rx.button(rx.text("Services", size="4", weight="medium"), rx.icon( + "chevron-down"), weight="medium", variant="ghost", size="3"), + ), + rx.menu.content( + rx.menu.item("Service 1"), + rx.menu.item("Service 2"), + rx.menu.item("Service 3"), + ), + ), + navbar_link("Pricing", "/#"), + navbar_link("Contact", "/#"), + justify="end", + spacing="5" + ), + justify="between", + align_items="center" + ), + ), + rx.mobile_and_tablet( + rx.hstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="6", weight="bold"), align_items="center"), + rx.menu.root( + rx.menu.trigger(rx.icon("menu", size=30)), + rx.menu.content( + rx.menu.item("Home"), + rx.menu.sub( + rx.menu.sub_trigger("Services"), + rx.menu.sub_content( + rx.menu.item("Service 1"), + rx.menu.item("Service 2"), + rx.menu.item("Service 3"), + ), + ), + rx.menu.item("About"), + rx.menu.item("Pricing"), + rx.menu.item("Contact"), + ), + justify="end", + ), + justify="between", + align_items="center" + ), + ), + bg=rx.color("accent", 3), + padding="1em", + # position="fixed", + # top="0px", + # z_index="5", + width="100%" + ) +``` + +## Search bar + +```python demo exec toggle +def navbar_searchbar() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.hstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), align_items="center"), + rx.input( + rx.input.slot(rx.icon("search")), + placeholder="Search...", + type="search", size="2", + justify="end", + ), + justify="between", + align_items="center" + ), + ), + rx.mobile_and_tablet( + rx.hstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="6", weight="bold"), align_items="center"), + rx.input( + rx.input.slot(rx.icon("search")), + placeholder="Search...", + type="search", size="2", + justify="end", + ), + justify="between", + align_items="center" + ), + ), + bg=rx.color("accent", 3), + padding="1em", + # position="fixed", + # top="0px", + # z_index="5", + width="100%" + ) +``` + +## Icons + +```python demo exec toggle +def navbar_icons_item(text: str, icon: str, url: str) -> rx.Component: + return rx.link(rx.hstack(rx.icon(icon), rx.text(text, size="4", weight="medium")), href=url) + +def navbar_icons_menu_item(text: str, icon: str, url: str) -> rx.Component: + return rx.link(rx.hstack(rx.icon(icon, size=16), rx.text(text, size="3", weight="medium")), href=url) + +def navbar_icons() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.hstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), align_items="center"), + rx.hstack( + navbar_icons_item("Home", "home", "/#"), + navbar_icons_item("Pricing", "coins", "/#"), + navbar_icons_item("Contact", "mail", "/#"), + navbar_icons_item("Services", "layers", "/#"), + spacing="6", + ), + justify="between", + align_items="center" + ), + ), + rx.mobile_and_tablet( + rx.hstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="6", weight="bold"), align_items="center"), + rx.menu.root( + rx.menu.trigger(rx.icon("menu", size=30)), + rx.menu.content( + navbar_icons_menu_item("Home", "home", "/#"), + navbar_icons_menu_item("Pricing", "coins", "/#"), + navbar_icons_menu_item("Contact", "mail", "/#"), + navbar_icons_menu_item("Services", "layers", "/#"), + ), + justify="end", + ), + justify="between", + align_items="center" + ), + ), + bg=rx.color("accent", 3), + padding="1em", + # position="fixed", + # top="0px", + # z_index="5", + width="100%" + ) +``` + +## Buttons + +```python demo exec toggle +def navbar_link(text: str, url: str) -> rx.Component: + return rx.link(rx.text(text, size="4", weight="medium"), href=url) + +def navbar_buttons() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.hstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), align_items="center"), + rx.hstack( + navbar_link("Home", "/#"), + navbar_link("About", "/#"), + navbar_link("Pricing", "/#"), + navbar_link("Contact", "/#"), + spacing="5", + ), + rx.hstack( + rx.button("Sign Up", size="3", variant="outline"), + rx.button("Log In", size="3"), + spacing="4", + justify="end", + ), + justify="between", + align_items="center" + ), + ), + rx.mobile_and_tablet( + rx.hstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="6", weight="bold"), align_items="center"), + rx.menu.root( + rx.menu.trigger(rx.icon("menu", size=30)), + rx.menu.content( + rx.menu.item("Home"), + rx.menu.item("About"), + rx.menu.item("Pricing"), + rx.menu.item("Contact"), + rx.menu.separator(), + rx.menu.item("Log in"), + rx.menu.item("Sign up"), + ), + justify="end", + ), + justify="between", + align_items="center" + ), + ), + bg=rx.color("accent", 3), + padding="1em", + # position="fixed", + # top="0px", + # z_index="5", + width="100%" + ) +``` + +## User profile + +```python demo exec toggle +def navbar_link(text: str, url: str) -> rx.Component: + return rx.link(rx.text(text, size="4", weight="medium"), href=url) + +def navbar_user() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.hstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.25em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), align_items="center"), + rx.hstack( + navbar_link("Home", "/#"), + navbar_link("About", "/#"), + navbar_link("Pricing", "/#"), + navbar_link("Contact", "/#"), + spacing="5", + ), + rx.menu.root( + rx.menu.trigger(rx.icon_button( + rx.icon("user"), size="2", radius="full")), + rx.menu.content( + rx.menu.item("Settings"), + rx.menu.item("Earnings"), + rx.menu.separator(), + rx.menu.item("Log out"), + ), + justify="end", + ), + justify="between", + align_items="center" + ), + ), + rx.mobile_and_tablet( + rx.hstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2em", height="auto", border_radius="25%"), + rx.heading("Reflex", size="6", weight="bold"), align_items="center"), + rx.menu.root( + rx.menu.trigger(rx.icon_button( + rx.icon("user"), size="2", radius="full")), + rx.menu.content( + rx.menu.item("Settings"), + rx.menu.item("Earnings"), + rx.menu.separator(), + rx.menu.item("Log out"), + ), + justify="end", + ), + justify="between", + align_items="center" + ), + ), + bg=rx.color("accent", 3), + padding="1em", + # position="fixed", + # top="0px", + # z_index="5", + width="100%" + ) +``` diff --git a/docs/recipes/layout/sidebar.md b/docs/recipes/layout/sidebar.md new file mode 100644 index 00000000000..e30f90d566c --- /dev/null +++ b/docs/recipes/layout/sidebar.md @@ -0,0 +1,421 @@ +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN + +def sidebar_item(text: str, icon: str, href: str) -> rx.Component: + return rx.link( + rx.hstack( + rx.icon(icon), + rx.text(text, size="4"), + width="100%", + padding_x="0.5rem", + padding_y="0.75rem", + align="center", + style={ + "_hover": { + "bg": rx.color("accent", 4), + "color": rx.color("accent", 11), + }, + "border-radius": "0.5em", + }, + ), + href=href, + underline="none", + weight="medium", + width="100%" + ) + +def sidebar_items() -> rx.Component: + return rx.vstack( + sidebar_item("Dashboard", "layout-dashboard", "/#"), + sidebar_item("Projects", "square-library", "/#"), + sidebar_item("Analytics", "bar-chart-4", "/#"), + sidebar_item("Messages", "mail", "/#"), + spacing="1", + width="100%" + ) +``` + +# Sidebar + +Similar to a navigation bar, a sidebar is a common UI element found on the side of a webpage or application. It typically contains links to different sections of the site or app. + +## Basic + +```python demo exec toggle +def sidebar_item(text: str, icon: str, href: str) -> rx.Component: + return rx.link( + rx.hstack( + rx.icon(icon), + rx.text(text, size="4"), + width="100%", + padding_x="0.5rem", + padding_y="0.75rem", + align="center", + style={ + "_hover": { + "bg": rx.color("accent", 4), + "color": rx.color("accent", 11), + }, + "border-radius": "0.5em", + }, + ), + href=href, + underline="none", + weight="medium", + width="100%" + ) + +def sidebar_items() -> rx.Component: + return rx.vstack( + sidebar_item("Dashboard", "layout-dashboard", "/#"), + sidebar_item("Projects", "square-library", "/#"), + sidebar_item("Analytics", "bar-chart-4", "/#"), + sidebar_item("Messages", "mail", "/#"), + spacing="1", + width="100%" + ) + +def sidebar() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.vstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.25em", + height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), + align="center", + justify="start", + padding_x="0.5rem", + width="100%" + ), + sidebar_items(), + spacing="5", + #position="fixed", + # left="0px", + # top="0px", + # z_index="5", + padding_x="1em", + padding_y="1.5em", + bg=rx.color("accent", 3), + align="start", + #height="100%", + height="650px", + width="16em", + ), + ), + rx.mobile_and_tablet( + rx.drawer.root( + rx.drawer.trigger(rx.icon("align-justify", size=30)), + rx.drawer.overlay(z_index="5"), + rx.drawer.portal( + rx.drawer.content( + rx.vstack( + rx.box( + rx.drawer.close(rx.icon("x", size=30)), + width="100%", + ), + sidebar_items(), + spacing="5", + width="100%", + ), + top="auto", + right="auto", + height="100%", + width="20em", + padding="1.5em", + bg=rx.color("accent", 2) + ), + width="100%", + ), + direction="left" + ), + padding="1em", + ), + ) +``` + +## Bottom user profile + +```python demo exec toggle +def sidebar_item(text: str, icon: str, href: str) -> rx.Component: + return rx.link( + rx.hstack( + rx.icon(icon), + rx.text(text, size="4"), + width="100%", + padding_x="0.5rem", + padding_y="0.75rem", + align="center", + style={ + "_hover": { + "bg": rx.color("accent", 4), + "color": rx.color("accent", 11), + }, + "border-radius": "0.5em", + }, + ), + href=href, + underline="none", + weight="medium", + width="100%" + ) + +def sidebar_items() -> rx.Component: + return rx.vstack( + sidebar_item("Dashboard", "layout-dashboard", "/#"), + sidebar_item("Projects", "square-library", "/#"), + sidebar_item("Analytics", "bar-chart-4", "/#"), + sidebar_item("Messages", "mail", "/#"), + spacing="1", + width="100%" + ) + +def sidebar_bottom_profile() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.vstack( + rx.hstack( + rx.image(src=f"{REFLEX_ASSETS_CDN}other/logo.jpg", width="2.25em", + height="auto", border_radius="25%"), + rx.heading("Reflex", size="7", weight="bold"), + align="center", + justify="start", + padding_x="0.5rem", + width="100%" + ), + sidebar_items(), + rx.spacer(), + rx.vstack( + rx.vstack( + sidebar_item("Settings", "settings", "/#"), + sidebar_item("Log out", "log-out", "/#"), + spacing="1", + width="100%" + ), + rx.divider(), + rx.hstack( + rx.icon_button(rx.icon("user"), size="3", radius="full"), + rx.vstack( + rx.box( + rx.text("My account", size="3", weight="bold"), + rx.text("user@reflex.dev", size="2", weight="medium"), + width="100%" + ), + spacing="0", + align="start", + justify="start", + width="100%" + ), + padding_x="0.5rem", + align="center", + justify="start", + width="100%", + ), + width="100%", + spacing="5", + ), + spacing="5", + #position="fixed", + # left="0px", + # top="0px", + # z_index="5", + padding_x="1em", + padding_y="1.5em", + bg=rx.color("accent", 3), + align="start", + #height="100%", + height="650px", + width="16em", + ), + ), + rx.mobile_and_tablet( + rx.drawer.root( + rx.drawer.trigger(rx.icon("align-justify", size=30)), + rx.drawer.overlay(z_index="5"), + rx.drawer.portal( + rx.drawer.content( + rx.vstack( + rx.box( + rx.drawer.close(rx.icon("x", size=30)), + width="100%", + ), + sidebar_items(), + rx.spacer(), + rx.vstack( + rx.vstack( + sidebar_item("Settings", "settings", "/#"), + sidebar_item("Log out", "log-out", "/#"), + width="100%", + spacing="1", + ), + rx.divider(margin="0"), + rx.hstack( + rx.icon_button(rx.icon("user"), size="3", radius="full"), + rx.vstack( + rx.box( + rx.text("My account", size="3", weight="bold"), + rx.text("user@reflex.dev", size="2", weight="medium"), + width="100%" + ), + spacing="0", + justify="start", + width="100%", + ), + padding_x="0.5rem", + align="center", + justify="start", + width="100%", + ), + width="100%", + spacing="5", + ), + spacing="5", + width="100%", + ), + top="auto", + right="auto", + height="100%", + width="20em", + padding="1.5em", + bg=rx.color("accent", 2) + ), + width="100%", + ), + direction="left" + ), + padding="1em", + ), + ) +``` + +## Top user profile + +```python demo exec toggle +def sidebar_item(text: str, icon: str, href: str) -> rx.Component: + return rx.link( + rx.hstack( + rx.icon(icon), + rx.text(text, size="4"), + width="100%", + padding_x="0.5rem", + padding_y="0.75rem", + align="center", + style={ + "_hover": { + "bg": rx.color("accent", 4), + "color": rx.color("accent", 11), + }, + "border-radius": "0.5em", + }, + ), + href=href, + underline="none", + weight="medium", + width="100%" + ) + +def sidebar_items() -> rx.Component: + return rx.vstack( + sidebar_item("Dashboard", "layout-dashboard", "/#"), + sidebar_item("Projects", "square-library", "/#"), + sidebar_item("Analytics", "bar-chart-4", "/#"), + sidebar_item("Messages", "mail", "/#"), + spacing="1", + width="100%" + ) + +def sidebar_top_profile() -> rx.Component: + return rx.box( + rx.desktop_only( + rx.vstack( + rx.hstack( + rx.icon_button(rx.icon("user"), size="3", radius="full"), + rx.vstack( + rx.box( + rx.text("My account", size="3", weight="bold"), + rx.text("user@reflex.dev", size="2", weight="medium"), + width="100%" + ), + spacing="0", + justify="start", + width="100%", + ), + rx.spacer(), + rx.icon_button(rx.icon("settings"), size="2", + variant="ghost", color_scheme="gray"), + padding_x="0.5rem", + align="center", + width="100%", + ), + sidebar_items(), + rx.spacer(), + sidebar_item("Help & Support", "life-buoy", "/#"), + spacing="5", + #position="fixed", + # left="0px", + # top="0px", + # z_index="5", + padding_x="1em", + padding_y="1.5em", + bg=rx.color("accent", 3), + align="start", + #height="100%", + height="650px", + width="16em", + ), + ), + rx.mobile_and_tablet( + rx.drawer.root( + rx.drawer.trigger(rx.icon("align-justify", size=30)), + rx.drawer.overlay(z_index="5"), + rx.drawer.portal( + rx.drawer.content( + rx.vstack( + rx.box( + rx.drawer.close(rx.icon("x", size=30)), + width="100%", + ), + sidebar_items(), + rx.spacer(), + rx.vstack( + sidebar_item("Help & Support", "life-buoy", "/#"), + rx.divider(margin="0"), + rx.hstack( + rx.icon_button(rx.icon("user"), size="3", radius="full"), + rx.vstack( + rx.box( + rx.text("My account", size="3", weight="bold"), + rx.text("user@reflex.dev", size="2", weight="medium"), + width="100%" + ), + spacing="0", + justify="start", + width="100%", + ), + padding_x="0.5rem", + align="center", + justify="start", + width="100%", + ), + width="100%", + spacing="5", + ), + spacing="5", + width="100%", + ), + top="auto", + right="auto", + height="100%", + width="20em", + padding="1.5em", + bg=rx.color("accent", 2) + ), + width="100%", + ), + direction="left" + ), + padding="1em", + ), + ) +``` diff --git a/docs/recipes/others/checkboxes.md b/docs/recipes/others/checkboxes.md new file mode 100644 index 00000000000..308211f95a5 --- /dev/null +++ b/docs/recipes/others/checkboxes.md @@ -0,0 +1,68 @@ +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +``` + +# Smart Checkboxes Group + +A smart checkboxes group where you can track all checked boxes, as well as place a limit on how many checks are possible. + +## Recipe + +```python eval +rx.center(rx.image(src=f"{REFLEX_ASSETS_CDN}templates/smart_checkboxes.webp")) +``` + +This recipe use a `dict[str, bool]` for the checkboxes state tracking. +Additionally, the limit that prevent the user from checking more boxes than allowed with a computed var. + +```python +class CBoxeState(rx.State): + + choices: dict[str, bool] = \{k: False for k in ["Choice A", "Choice B", "Choice C"]} + _check_limit = 2 + + def check_choice(self, value, index): + self.choices[index] = value + + @rx.var + def choice_limit(self): + return sum(self.choices.values()) >= self._check_limit + + @rx.var + def checked_choices(self): + choices = [l for l, v in self.choices.items() if v] + return " / ".join(choices) if choices else "None" + +import reflex as rx + + +def render_checkboxes(values, limit, handler): + return rx.vstack( + rx.foreach( + values, + lambda choice: rx.checkbox( + choice[0], + checked=choice[1], + disabled=~choice[1] & limit, + on_change=lambda val: handler(val, choice[0]), + ), + ) + ) + + +def index() -> rx.Component: + + return rx.center( + rx.vstack( + rx.text("Make your choices (2 max):"), + render_checkboxes( + CBoxeState.choices, + CBoxeState.choice_limit, + CBoxeState.check_choice, + ), + rx.text("Your choices: ", CBoxeState.checked_choices), + ), + height="100vh", + ) +``` diff --git a/docs/recipes/others/chips.md b/docs/recipes/others/chips.md new file mode 100644 index 00000000000..c72ab54aa50 --- /dev/null +++ b/docs/recipes/others/chips.md @@ -0,0 +1,247 @@ +```python exec +import reflex as rx + +``` + +# Chips + +Chips are compact elements that represent small pieces of information, such as tags or categories. They are commonly used to select multiple items from a list or to filter content. + +## Status + +```python demo exec toggle +from reflex.components.radix.themes.base import LiteralAccentColor + +status_chip_props = { + "radius": "full", + "variant": "outline", + "size": "3", +} + +def status_chip(status: str, icon: str, color: LiteralAccentColor) -> rx.Component: + return rx.badge( + rx.icon(icon, size=18), + status, + color_scheme=color, + **status_chip_props, + ) + +def status_chips_group() -> rx.Component: + return rx.hstack( + status_chip("Info", "info", "blue"), + status_chip("Success", "circle-check", "green"), + status_chip("Warning", "circle-alert", "yellow"), + status_chip("Error", "circle-x", "red"), + wrap="wrap", + spacing="2", + ) +``` + +## Single selection + +```python demo exec toggle +chip_props = { + "radius": "full", + "variant": "soft", + "size": "3", + "cursor": "pointer", + "style": {"_hover": {"opacity": 0.75}}, +} + +available_items = ["2:00", "3:00", "4:00", "5:00"] + +class SingleSelectionChipsState(rx.State): + selected_item: str = "" + + @rx.event + def set_selected_item(self, value: str): + self.selected_item = value + +def unselected_item(item: str) -> rx.Component: + return rx.badge( + item, + color_scheme="gray", + **chip_props, + on_click=SingleSelectionChipsState.set_selected_item(item), + ) + +def selected_item(item: str) -> rx.Component: + return rx.badge( + rx.icon("check", size=18), + item, + color_scheme="mint", + **chip_props, + on_click=SingleSelectionChipsState.set_selected_item(""), + ) + +def item_chip(item: str) -> rx.Component: + return rx.cond( + SingleSelectionChipsState.selected_item == item, + selected_item(item), + unselected_item(item), + ) + +def item_selector() -> rx.Component: + return rx.vstack( + rx.hstack( + rx.icon("clock", size=20), + rx.heading( + "Select your reservation time:", size="4" + ), + spacing="2", + align="center", + width="100%", + ), + rx.hstack( + rx.foreach(available_items, item_chip), + wrap="wrap", + spacing="2", + ), + align_items="start", + spacing="4", + width="100%", + ) +``` + +## Multiple selection + +This example demonstrates selecting multiple skills from a list. It includes buttons to add all skills, clear selected skills, and select a random number of skills. + +```python demo exec toggle +import random +from reflex.components.radix.themes.base import LiteralAccentColor + +chip_props = { + "radius": "full", + "variant": "surface", + "size": "3", + "cursor": "pointer", + "style": {"_hover": {"opacity": 0.75}}, +} + +skills = [ + "Data Management", + "Networking", + "Security", + "Cloud", + "DevOps", + "Data Science", + "AI", + "ML", + "Robotics", + "Cybersecurity", +] + +class BasicChipsState(rx.State): + selected_items: list[str] = skills[:3] + + @rx.event + def add_selected(self, item: str): + self.selected_items.append(item) + + @rx.event + def remove_selected(self, item: str): + self.selected_items.remove(item) + + @rx.event + def add_all_selected(self): + self.selected_items = list(skills) + + @rx.event + def clear_selected(self): + self.selected_items.clear() + + @rx.event + def random_selected(self): + self.selected_items = random.sample(skills, k=random.randint(1, len(skills))) + +def action_button(icon: str, label: str, on_click: callable, color_scheme: LiteralAccentColor) -> rx.Component: + return rx.button( + rx.icon(icon, size=16), + label, + variant="soft", + size="2", + on_click=on_click, + color_scheme=color_scheme, + cursor="pointer", + ) + +def selected_item_chip(item: str) -> rx.Component: + return rx.badge( + item, + rx.icon("circle-x", size=18), + color_scheme="green", + **chip_props, + on_click=BasicChipsState.remove_selected(item), + ) + +def unselected_item_chip(item: str) -> rx.Component: + return rx.cond( + BasicChipsState.selected_items.contains(item), + rx.fragment(), + rx.badge( + item, + rx.icon("circle-plus", size=18), + color_scheme="gray", + **chip_props, + on_click=BasicChipsState.add_selected(item), + ), + ) + +def items_selector() -> rx.Component: + return rx.vstack( + rx.flex( + rx.hstack( + rx.icon("lightbulb", size=20), + rx.heading( + "Skills" + f" ({BasicChipsState.selected_items.length()})", size="4" + ), + spacing="1", + align="center", + width="100%", + justify_content=["end", "start"], + ), + rx.hstack( + action_button( + "plus", "Add All", BasicChipsState.add_all_selected, "green" + ), + action_button( + "trash", "Clear All", BasicChipsState.clear_selected, "tomato" + ), + action_button( + "shuffle", "", BasicChipsState.random_selected, "gray" + ), + spacing="2", + justify="end", + width="100%", + ), + justify="between", + flex_direction=["column", "row"], + align="center", + spacing="2", + margin_bottom="10px", + width="100%", + ), + # Selected Items + rx.flex( + rx.foreach( + BasicChipsState.selected_items, + selected_item_chip, + ), + wrap="wrap", + spacing="2", + justify_content="start", + ), + rx.divider(), + # Unselected Items + rx.flex( + rx.foreach(skills, unselected_item_chip), + wrap="wrap", + spacing="2", + justify_content="start", + ), + justify_content="start", + align_items="start", + width="100%", + ) +``` diff --git a/docs/recipes/others/dark_mode_toggle.md b/docs/recipes/others/dark_mode_toggle.md new file mode 100644 index 00000000000..c648941157e --- /dev/null +++ b/docs/recipes/others/dark_mode_toggle.md @@ -0,0 +1,33 @@ +```python exec +import reflex as rx +from reflex.style import set_color_mode, color_mode +``` + +# Dark Mode Toggle + +The Dark Mode Toggle component lets users switch between light and dark themes. + +```python demo exec toggle +import reflex as rx +from reflex.style import set_color_mode, color_mode + +def dark_mode_toggle() -> rx.Component: + return rx.segmented_control.root( + rx.segmented_control.item( + rx.icon(tag="monitor", size=20), + value="system", + ), + rx.segmented_control.item( + rx.icon(tag="sun", size=20), + value="light", + ), + rx.segmented_control.item( + rx.icon(tag="moon", size=20), + value="dark", + ), + on_change=set_color_mode, + variant="classic", + radius="large", + value=color_mode, + ) +``` diff --git a/docs/recipes/others/pricing_cards.md b/docs/recipes/others/pricing_cards.md new file mode 100644 index 00000000000..0d88ae4824c --- /dev/null +++ b/docs/recipes/others/pricing_cards.md @@ -0,0 +1,198 @@ +```python exec +import reflex as rx +``` + +# Pricing Cards + +A pricing card shows the price of a product or service. It typically includes a title, description, price, features, and a purchase button. + +## Basic + +```python demo exec toggle +def feature_item(text: str) -> rx.Component: + return rx.hstack(rx.icon("check", color=rx.color("grass", 9)), rx.text(text, size="4")) + +def features() -> rx.Component: + return rx.vstack( + feature_item("24/7 customer support"), + feature_item("Daily backups"), + feature_item("Advanced analytics"), + feature_item("Customizable templates"), + feature_item("Priority email support"), + width="100%", + align_items="start", + ) + +def pricing_card_beginner() -> rx.Component: + return rx.vstack( + rx.vstack( + rx.text("Beginner", weight="bold", size="6"), + rx.text("Ideal choice for personal use & for your next project.", size="4", opacity=0.8, align="center"), + rx.hstack( + rx.text("$39", weight="bold", font_size="3rem", trim="both"), + rx.text("/month", size="4", opacity=0.8, trim="both"), + width="100%", + align_items="end", + justify="center" + ), + width="100%", + align="center", + spacing="6", + ), + features(), + rx.button("Get started", size="3", variant="solid", width="100%", color_scheme="blue"), + spacing="6", + border=f"1.5px solid {rx.color('gray', 5)}", + background=rx.color("gray", 1), + padding="28px", + width="100%", + max_width="400px", + justify="center", + border_radius="0.5rem", + ) +``` + +## Comparison cards + +```python demo exec toggle +def feature_item(feature: str) -> rx.Component: + return rx.hstack( + rx.icon("check", color=rx.color("blue", 9), size=21), + rx.text(feature, size="4", weight="regular"), + ) + + +def standard_features() -> rx.Component: + return rx.vstack( + feature_item("40 credits for image generation"), + feature_item("Credits never expire"), + feature_item("High quality images"), + feature_item("Commercial license"), + spacing="3", + width="100%", + align_items="start", + ) + + +def popular_features() -> rx.Component: + return rx.vstack( + feature_item("250 credits for image generation"), + feature_item("+30% Extra free credits"), + feature_item("Credits never expire"), + feature_item("High quality images"), + feature_item("Commercial license"), + spacing="3", + width="100%", + align_items="start", + ) + + +def pricing_card_standard() -> rx.Component: + return rx.vstack( + rx.hstack( + rx.hstack( + rx.text( + "$14.99", + trim="both", + as_="s", + size="3", + weight="regular", + opacity=0.8, + ), + rx.text("$3.99", trim="both", size="6", weight="regular"), + width="100%", + spacing="2", + align_items="end", + ), + height="35px", + align_items="center", + justify="between", + width="100%", + ), + rx.text( + "40 Image Credits", + weight="bold", + size="7", + width="100%", + text_align="left", + ), + standard_features(), + rx.spacer(), + rx.button( + "Purchase", + size="3", + variant="outline", + width="100%", + color_scheme="blue", + ), + spacing="6", + border=f"1.5px solid {rx.color('gray', 5)}", + background=rx.color("gray", 1), + padding="28px", + width="100%", + max_width="400px", + min_height="475px", + border_radius="0.5rem", + ) + + +def pricing_card_popular() -> rx.Component: + return rx.vstack( + rx.hstack( + rx.hstack( + rx.text( + "$69.99", + trim="both", + as_="s", + size="3", + weight="regular", + opacity=0.8, + ), + rx.text("$18.99", trim="both", size="6", weight="regular"), + width="100%", + spacing="2", + align_items="end", + ), + rx.badge( + "POPULAR", + size="2", + radius="full", + variant="soft", + color_scheme="blue", + ), + align_items="center", + justify="between", + height="35px", + width="100%", + ), + rx.text( + "250 Image Credits", + weight="bold", + size="7", + width="100%", + text_align="left", + ), + popular_features(), + rx.spacer(), + rx.button("Purchase", size="3", width="100%", color_scheme="blue"), + spacing="6", + border=f"1.5px solid {rx.color('blue', 6)}", + background=rx.color("blue", 1), + padding="28px", + width="100%", + max_width="400px", + min_height="475px", + border_radius="0.5rem", + ) + + +def pricing_cards() -> rx.Component: + return rx.flex( + pricing_card_standard(), + pricing_card_popular(), + spacing="4", + flex_direction=["column", "column", "row"], + width="100%", + align_items="center", + ) +``` diff --git a/docs/recipes/others/speed_dial.md b/docs/recipes/others/speed_dial.md new file mode 100644 index 00000000000..4d2086ec080 --- /dev/null +++ b/docs/recipes/others/speed_dial.md @@ -0,0 +1,454 @@ +```python exec +import reflex as rx +``` + +# Speed Dial + +A speed dial is a component that allows users to quickly access frequently used actions or pages. It is often used in the bottom right corner of the screen. + +# Vertical + +```python demo exec toggle +class SpeedDialVertical(rx.ComponentState): + is_open: bool = False + + @rx.event + def toggle(self, value: bool): + self.is_open = value + + @classmethod + def get_component(cls, **props): + def menu_item(icon: str, text: str) -> rx.Component: + return rx.tooltip( + rx.icon_button( + rx.icon(icon, padding="2px"), + variant="soft", + color_scheme="gray", + size="3", + cursor="pointer", + radius="full", + ), + side="left", + content=text, + ) + + def menu() -> rx.Component: + return rx.vstack( + menu_item("copy", "Copy"), + menu_item("download", "Download"), + menu_item("share-2", "Share"), + position="absolute", + bottom="100%", + spacing="2", + padding_bottom="10px", + left="0", + direction="column-reverse", + align_items="center", + ) + + return rx.box( + rx.box( + rx.icon_button( + rx.icon( + "plus", + style={ + "transform": rx.cond(cls.is_open, "rotate(45deg)", "rotate(0)"), + "transition": "transform 150ms cubic-bezier(0.4, 0, 0.2, 1)", + }, + ), + variant="solid", + color_scheme="blue", + size="3", + cursor="pointer", + radius="full", + position="relative", + ), + rx.cond( + cls.is_open, + menu(), + ), + position="relative", + ), + on_mouse_enter=cls.toggle(True), + on_mouse_leave=cls.toggle(False), + on_click=cls.toggle(~cls.is_open), + style={"bottom": "15px", "right": "15px"}, + position="absolute", + # z_index="50", + **props, + ) + +speed_dial_vertical = SpeedDialVertical.create + +def render_vertical(): + return rx.box( + speed_dial_vertical(), + height="250px", + position="relative", + width="100%", + ) +``` + +# Horizontal + +```python demo exec toggle +class SpeedDialHorizontal(rx.ComponentState): + is_open: bool = False + + @rx.event + def toggle(self, value: bool): + self.is_open = value + + @classmethod + def get_component(cls, **props): + def menu_item(icon: str, text: str) -> rx.Component: + return rx.tooltip( + rx.icon_button( + rx.icon(icon, padding="2px"), + variant="soft", + color_scheme="gray", + size="3", + cursor="pointer", + radius="full", + ), + side="top", + content=text, + ) + + def menu() -> rx.Component: + return rx.hstack( + menu_item("copy", "Copy"), + menu_item("download", "Download"), + menu_item("share-2", "Share"), + position="absolute", + bottom="0", + spacing="2", + padding_right="10px", + right="100%", + direction="row-reverse", + align_items="center", + ) + + return rx.box( + rx.box( + rx.icon_button( + rx.icon( + "plus", + style={ + "transform": rx.cond(cls.is_open, "rotate(45deg)", "rotate(0)"), + "transition": "transform 150ms cubic-bezier(0.4, 0, 0.2, 1)", + }, + class_name="dial", + ), + variant="solid", + color_scheme="green", + size="3", + cursor="pointer", + radius="full", + position="relative", + ), + rx.cond( + cls.is_open, + menu(), + ), + position="relative", + ), + on_mouse_enter=cls.toggle(True), + on_mouse_leave=cls.toggle(False), + on_click=cls.toggle(~cls.is_open), + style={"bottom": "15px", "right": "15px"}, + position="absolute", + # z_index="50", + **props, + ) + +speed_dial_horizontal = SpeedDialHorizontal.create + +def render_horizontal(): + return rx.box( + speed_dial_horizontal(), + height="250px", + position="relative", + width="100%", + ) +``` + +# Vertical with text + +```python demo exec toggle +class SpeedDialVerticalText(rx.ComponentState): + is_open: bool = False + + @rx.event + def toggle(self, value: bool): + self.is_open = value + + @classmethod + def get_component(cls, **props): + def menu_item(icon: str, text: str) -> rx.Component: + return rx.hstack( + rx.text(text, weight="medium"), + rx.icon_button( + rx.icon(icon, padding="2px"), + variant="soft", + color_scheme="gray", + size="3", + cursor="pointer", + radius="full", + position="relative", + ), + opacity="0.75", + _hover={ + "opacity": "1", + }, + align_items="center", + ) + + def menu() -> rx.Component: + return rx.vstack( + menu_item("copy", "Copy"), + menu_item("download", "Download"), + menu_item("share-2", "Share"), + position="absolute", + bottom="100%", + spacing="2", + padding_bottom="10px", + right="0", + direction="column-reverse", + align_items="end", + justify_content="end", + ) + + return rx.box( + rx.box( + rx.icon_button( + rx.icon( + "plus", + style={ + "transform": rx.cond(cls.is_open, "rotate(45deg)", "rotate(0)"), + "transition": "transform 150ms cubic-bezier(0.4, 0, 0.2, 1)", + }, + class_name="dial", + ), + variant="solid", + color_scheme="crimson", + size="3", + cursor="pointer", + radius="full", + position="relative", + ), + rx.cond( + cls.is_open, + menu(), + ), + position="relative", + ), + on_mouse_enter=cls.toggle(True), + on_mouse_leave=cls.toggle(False), + on_click=cls.toggle(~cls.is_open), + style={"bottom": "15px", "right": "15px"}, + position="absolute", + # z_index="50", + **props, + ) + +speed_dial_vertical_text = SpeedDialVerticalText.create + +def render_vertical_text(): + return rx.box( + speed_dial_vertical_text(), + height="250px", + position="relative", + width="100%", + ) +``` + +# Reveal animation + +```python demo exec toggle +class SpeedDialReveal(rx.ComponentState): + is_open: bool = False + + @rx.event + def toggle(self, value: bool): + self.is_open = value + + @classmethod + def get_component(cls, **props): + def menu_item(icon: str, text: str) -> rx.Component: + return rx.tooltip( + rx.icon_button( + rx.icon(icon, padding="2px"), + variant="soft", + color_scheme="gray", + size="3", + cursor="pointer", + radius="full", + style={ + "animation": rx.cond(cls.is_open, "reveal 0.3s ease both", "none"), + "@keyframes reveal": { + "0%": { + "opacity": "0", + "transform": "scale(0)", + }, + "100%": { + "opacity": "1", + "transform": "scale(1)", + }, + }, + }, + ), + side="left", + content=text, + ) + + def menu() -> rx.Component: + return rx.vstack( + menu_item("copy", "Copy"), + menu_item("download", "Download"), + menu_item("share-2", "Share"), + position="absolute", + bottom="100%", + spacing="2", + padding_bottom="10px", + left="0", + direction="column-reverse", + align_items="center", + ) + + return rx.box( + rx.box( + rx.icon_button( + rx.icon( + "plus", + style={ + "transform": rx.cond(cls.is_open, "rotate(45deg)", "rotate(0)"), + "transition": "transform 150ms cubic-bezier(0.4, 0, 0.2, 1)", + }, + class_name="dial", + ), + variant="solid", + color_scheme="violet", + size="3", + cursor="pointer", + radius="full", + position="relative", + ), + rx.cond( + cls.is_open, + menu(), + ), + position="relative", + ), + on_mouse_enter=cls.toggle(True), + on_mouse_leave=cls.toggle(False), + on_click=cls.toggle(~cls.is_open), + style={"bottom": "15px", "right": "15px"}, + position="absolute", + # z_index="50", + **props, + ) + +speed_dial_reveal = SpeedDialReveal.create + +def render_reveal(): + return rx.box( + speed_dial_reveal(), + height="250px", + position="relative", + width="100%", + ) +``` + +# Menu + +```python demo exec toggle +class SpeedDialMenu(rx.ComponentState): + is_open: bool = False + + @rx.event + def toggle(self, value: bool): + self.is_open = value + + @classmethod + def get_component(cls, **props): + def menu_item(icon: str, text: str) -> rx.Component: + return rx.hstack( + rx.icon(icon, padding="2px"), + rx.text(text, weight="medium"), + align="center", + opacity="0.75", + cursor="pointer", + position="relative", + _hover={ + "opacity": "1", + }, + width="100%", + align_items="center", + ) + + def menu() -> rx.Component: + return rx.box( + rx.card( + rx.vstack( + menu_item("copy", "Copy"), + rx.divider(margin="0"), + menu_item("download", "Download"), + rx.divider(margin="0"), + menu_item("share-2", "Share"), + direction="column-reverse", + align_items="end", + justify_content="end", + ), + box_shadow="0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", + ), + position="absolute", + bottom="100%", + right="0", + padding_bottom="10px", + ) + + return rx.box( + rx.box( + rx.icon_button( + rx.icon( + "plus", + style={ + "transform": rx.cond(cls.is_open, "rotate(45deg)", "rotate(0)"), + "transition": "transform 150ms cubic-bezier(0.4, 0, 0.2, 1)", + }, + class_name="dial", + ), + variant="solid", + color_scheme="orange", + size="3", + cursor="pointer", + radius="full", + position="relative", + ), + rx.cond( + cls.is_open, + menu(), + ), + position="relative", + ), + on_mouse_enter=cls.toggle(True), + on_mouse_leave=cls.toggle(False), + on_click=cls.toggle(~cls.is_open), + style={"bottom": "15px", "right": "15px"}, + position="absolute", + # z_index="50", + **props, + ) + + +speed_dial_menu = SpeedDialMenu.create + +def render_menu(): + return rx.box( + speed_dial_menu(), + height="250px", + position="relative", + width="100%", + ) +``` diff --git a/docs/state/overview.md b/docs/state/overview.md new file mode 100644 index 00000000000..a9397a3da19 --- /dev/null +++ b/docs/state/overview.md @@ -0,0 +1,189 @@ +```python exec +import reflex as rx +from pcweb.templates.docpage import definition +``` + +# State + +State allows us to create interactive apps that can respond to user input. +It defines the variables that can change over time, and the functions that can modify them. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=1206&end=1869 +# Video: State Overview +``` + +## State Basics + +You can define state by creating a class that inherits from `rx.State`: + +```python +import reflex as rx + + +class State(rx.State): + """Define your app state here.""" +``` + +A state class is made up of two parts: vars and event handlers. + +**Vars** are variables in your app that can change over time. + +**Event handlers** are functions that modify these vars in response to events. + +These are the main concepts to understand how state works in Reflex: + +```python eval +rx.grid( + definition( + "Base Var", + rx.list.unordered( + rx.list.item("Any variable in your app that can change over time."), + rx.list.item( + "Defined as a field in a ", rx.code("State"), " class" + ), + rx.list.item("Can only be modified by event handlers."), + ), + ), + definition( + "Computed Var", + rx.list.unordered( + rx.list.item("Vars that change automatically based on other vars."), + rx.list.item( + "Defined as functions using the ", + rx.code("@rx.var"), + " decorator.", + ), + rx.list.item( + "Cannot be set by event handlers, are always recomputed when the state changes." + ), + ), + ), + definition( + "Event Trigger", + rx.list.unordered( + rx.list.item( + "A user interaction that triggers an event, such as a button click." + ), + rx.list.item( + "Defined as special component props, such as ", + rx.code("on_click"), + ".", + ), + rx.list.item("Can be used to trigger event handlers."), + ), + ), + definition( + "Event Handlers", + rx.list.unordered( + rx.list.item( + "Functions that update the state in response to events." + ), + rx.list.item( + "Defined as methods in the ", rx.code("State"), " class." + ), + rx.list.item( + "Can be called by event triggers, or by other event handlers." + ), + ), + ), + margin_bottom="1em", + spacing="2", + columns="2", +) +``` + +## Example + +Here is a example of how to use state within a Reflex app. +Click the text to change its color. + +```python demo exec +class ExampleState(rx.State): + + # A base var for the list of colors to cycle through. + colors: list[str] = ["black", "red", "green", "blue", "purple"] + + # A base var for the index of the current color. + index: int = 0 + + @rx.event + def next_color(self): + """An event handler to go to the next color.""" + # Event handlers can modify the base vars. + # Here we reference the base vars `colors` and `index`. + self.index = (self.index + 1) % len(self.colors) + + @rx.var + def color(self)-> str: + """A computed var that returns the current color.""" + # Computed vars update automatically when the state changes. + return self.colors[self.index] + + +def index(): + return rx.heading( + "Welcome to Reflex!", + # Event handlers can be bound to event triggers. + on_click=ExampleState.next_color, + # State vars can be bound to component props. + color=ExampleState.color, + _hover={"cursor": "pointer"}, + ) +``` + +The base vars are `colors` and `index`. They are the only vars in the app that +may be directly modified within event handlers. + +There is a single computed var, `color`, that is a function of the base vars. It +will be computed automatically whenever the base vars change. + +The heading component links its `on_click` event to the +`ExampleState.next_color` event handler, which increments the color index. + +```md alert success +# With Reflex, you never have to write an API. + +All interactions between the frontend and backend are handled through events. +``` + +```md alert info +# State vs. Instance? + +When building the UI of your app, reference vars and event handlers via the state class (`ExampleState`). + +When writing backend event handlers, access and set vars via the instance (`self`). +``` + +```md alert warning +# Cannot print a State var. + +The code `print(ExampleState.index)` will not work because the State var values are only known at compile time. +``` + +## Client States + +Each user who opens your app has a unique ID and their own copy of the state. +This means that each user can interact with the app and modify the state +independently of other users. + +Because Reflex internally creates a new instance of the state for each user, your code should +never directly initialize a state class. + +```md alert info +# Try opening an app in multiple tabs to see how the state changes independently. +``` + +All user state is stored on the server, and all event handlers are executed on +the server. Reflex uses websockets to send events to the server, and to send +state updates back to the client. + +## Helper Methods + +Similar to backend vars, any method defined in a State class that begins with an +underscore `_` is considered a helper method. Such methods are not usable as +event triggers, but may be called from other event handler methods within the +state. + +Functionality that should only be available on the backend, such as an +authenticated action, should use helper methods to ensure it is not accidentally +or maliciously triggered by the client. diff --git a/docs/state_structure/component_state.md b/docs/state_structure/component_state.md new file mode 100644 index 00000000000..174f90d52b8 --- /dev/null +++ b/docs/state_structure/component_state.md @@ -0,0 +1,233 @@ +```python exec +import reflex as rx +from pcweb.pages.docs import events, ui, vars +``` + +# Component State + +_New in version 0.4.6_. + +Defining a subclass of `rx.ComponentState` creates a special type of state that is tied to an +instance of a component, rather than existing globally in the app. A Component State combines +[UI code]({ui.overview.path}) with state [Vars]({vars.base_vars.path}) and +[Event Handlers]({events.events_overview.path}), +and is useful for creating reusable components which operate independently of each other. + +```md alert warning +# ComponentState cannot be used inside `rx.foreach()` as it will only create one state instance for all elements in the loop. Each iteration of the foreach will share the same state, which may lead to unexpected behavior. +``` + +## Using ComponentState + +```python demo exec +class ReusableCounter(rx.ComponentState): + count: int = 0 + + @rx.event + def set_count(self, value: int): + self.count = value + + @rx.event + def increment(self): + self.count += 1 + + @rx.event + def decrement(self): + self.count -= 1 + + @classmethod + def get_component(cls, **props): + return rx.hstack( + rx.button("Decrement", on_click=cls.decrement), + rx.text(cls.count), + rx.button("Increment", on_click=cls.increment), + **props, + ) + +reusable_counter = ReusableCounter.create + +def multiple_counters(): + return rx.vstack( + reusable_counter(), + reusable_counter(), + reusable_counter(), + ) +``` + +The vars and event handlers defined on the `ReusableCounter` +class are treated similarly to a normal State class, but will be scoped to the component instance. Each time a +`reusable_counter` is created, a new state class for that instance of the component is also created. + +The `get_component` classmethod is used to define the UI for the component and link it up to the State, which +is accessed via the `cls` argument. Other states may also be referenced by the returned component, but +`cls` will always be the instance of the `ComponentState` that is unique to the component being returned. + +## Passing Props + +Similar to a normal Component, the `ComponentState.create` classmethod accepts the arbitrary +`*children` and `**props` arguments, and by default passes them to your `get_component` classmethod. +These arguments may be used to customize the component, either by applying defaults or +passing props to certain subcomponents. + +```python eval +rx.divider() +``` + +In the following example, we implement an editable text component that allows the user to click on +the text to turn it into an input field. If the user does not provide their own `value` or `on_change` +props, then the defaults defined in the `EditableText` class will be used. + +```python demo exec +class EditableText(rx.ComponentState): + text: str = "Click to edit" + original_text: str + editing: bool = False + + @rx.event + def set_text(self, value: str): + self.text = value + + @rx.event + def start_editing(self, original_text: str): + self.original_text = original_text + self.editing = True + + @rx.event + def stop_editing(self): + self.editing = False + self.original_text = "" + + @classmethod + def get_component(cls, **props): + # Pop component-specific props with defaults before passing **props + value = props.pop("value", cls.text) + on_change = props.pop("on_change", cls.set_text) + cursor = props.pop("cursor", "pointer") + + # Set the initial value of the State var. + initial_value = props.pop("initial_value", None) + if initial_value is not None: + # Update the pydantic model to use the initial value as default. + cls.__fields__["text"].default = initial_value + + # Form elements for editing, saving and reverting the text. + edit_controls = rx.hstack( + rx.input( + value=value, + on_change=on_change, + **props, + ), + rx.icon_button( + rx.icon("x"), + on_click=[ + on_change(cls.original_text), + cls.stop_editing, + ], + type="button", + color_scheme="red", + ), + rx.icon_button(rx.icon("check")), + align="center", + width="100%", + ) + + # Return the text or the form based on the editing Var. + return rx.cond( + cls.editing, + rx.form( + edit_controls, + on_submit=lambda _: cls.stop_editing(), + ), + rx.text( + value, + on_click=cls.start_editing(value), + cursor=cursor, + **props, + ), + ) + + +editable_text = EditableText.create + + +def editable_text_example(): + return rx.vstack( + editable_text(), + editable_text(initial_value="Edit me!", color="blue"), + editable_text(initial_value="Reflex is fun", font_family="monospace", width="100%"), + ) +``` + +```python eval +rx.divider() +``` + +Because this `EditableText` component is designed to be reusable, it can handle the case +where the `value` and `on_change` are linked to a normal global state. + +```python exec +# Hack because flexdown re-inits modules +EditableText._per_component_state_instance_count = 4 +``` + +```python demo exec +class EditableTextDemoState(rx.State): + value: str = "Global state text" + + @rx.event + def set_value(self, value: str): + self.value = value + +def editable_text_with_global_state(): + return rx.vstack( + editable_text(value=EditableTextDemoState.value, on_change=EditableTextDemoState.set_value), + rx.text(EditableTextDemoState.value.upper()), + ) +``` + +## Accessing the State + +The underlying state class of a `ComponentState` is accessible via the `.State` attribute. To use it, +assign an instance of the component to a local variable, then include that instance in the page. + +```python exec +# Hack because flexdown re-inits modules +ReusableCounter._per_component_state_instance_count = 4 +``` + +```python demo exec +def counter_sum(): + counter1 = reusable_counter() + counter2 = reusable_counter() + return rx.vstack( + rx.text(f"Total: {counter1.State.count + counter2.State.count}"), + counter1, + counter2, + ) +``` + +```python eval +rx.divider() +``` + +Other components can also affect a `ComponentState` by referencing its event handlers or vars +via the `.State` attribute. + +```python exec +# Hack because flexdown re-inits modules +ReusableCounter._per_component_state_instance_count = 6 +``` + +```python demo exec +def extended_counter(): + counter1 = reusable_counter() + return rx.vstack( + counter1, + rx.hstack( + rx.icon_button(rx.icon("step_back"), on_click=counter1.State.set_count(0)), + rx.icon_button(rx.icon("plus"), on_click=counter1.State.increment), + rx.button("Double", on_click=counter1.State.set_count(counter1.State.count * 2)), + rx.button("Triple", on_click=counter1.State.set_count(counter1.State.count * 3)), + ), + ) +``` diff --git a/docs/state_structure/mixins.md b/docs/state_structure/mixins.md new file mode 100644 index 00000000000..566f50de8f1 --- /dev/null +++ b/docs/state_structure/mixins.md @@ -0,0 +1,329 @@ +```python exec +import reflex as rx +from pcweb.templates.docpage import definition +``` + +# State Mixins + +State mixins allow you to define shared functionality that can be reused across multiple State classes. This is useful for creating reusable components, shared business logic, or common state patterns. + +## What are State Mixins? + +A state mixin is a State class marked with `mixin=True` that cannot be instantiated directly but can be inherited by other State classes. Mixins provide a way to share: + +- Base variables +- Computed variables +- Event handlers +- Backend variables + +## Basic Mixin Definition + +To create a state mixin, inherit from `rx.State` and pass `mixin=True`: + +```python demo exec +class CounterMixin(rx.State, mixin=True): + count: int = 0 + + @rx.var + def count_display(self) -> str: + return f"Count: {self.count}" + + @rx.event + def increment(self): + self.count += 1 + +class MyState(CounterMixin, rx.State): + name: str = "App" + +def counter_example(): + return rx.vstack( + rx.heading(MyState.name), + rx.text(MyState.count_display), + rx.button("Increment", on_click=MyState.increment), + spacing="4", + align="center", + ) +``` + +In this example, `MyState` automatically inherits the `count` variable, `count_display` computed variable, and `increment` event handler from `CounterMixin`. + +## Multiple Mixin Inheritance + +You can inherit from multiple mixins to combine different pieces of functionality: + +```python demo exec +class TimestampMixin(rx.State, mixin=True): + last_updated: str = "" + + @rx.event + def update_timestamp(self): + import datetime + self.last_updated = datetime.datetime.now().strftime("%H:%M:%S") + +class LoggingMixin(rx.State, mixin=True): + log_messages: list[str] = [] + + @rx.event + def log_message(self, message: str): + self.log_messages.append(message) + +class CombinedState(CounterMixin, TimestampMixin, LoggingMixin, rx.State): + app_name: str = "Multi-Mixin App" + + @rx.event + def increment_with_log(self): + self.increment() + self.update_timestamp() + self.log_message(f"Count incremented to {self.count}") + +def multi_mixin_example(): + return rx.vstack( + rx.heading(CombinedState.app_name), + rx.text(CombinedState.count_display), + rx.text(f"Last updated: {CombinedState.last_updated}"), + rx.button("Increment & Log", on_click=CombinedState.increment_with_log), + rx.cond( + CombinedState.log_messages.length() > 0, + rx.vstack( + rx.foreach( + CombinedState.log_messages[-3:], + rx.text + ), + spacing="1" + ), + rx.text("No logs yet") + ), + spacing="4", + align="center", + ) +``` + +## Backend Variables in Mixins + +Mixins can also include backend variables (prefixed with `_`) that are not sent to the client: + +```python demo exec +class DatabaseMixin(rx.State, mixin=True): + _db_connection: dict = {} # Backend only + user_count: int = 0 # Sent to client + + @rx.event + def fetch_user_count(self): + # Simulate database query + self.user_count = len(self._db_connection.get("users", [])) + +class AppState(DatabaseMixin, rx.State): + app_title: str = "User Management" + +def database_example(): + return rx.vstack( + rx.heading(AppState.app_title), + rx.text(f"User count: {AppState.user_count}"), + rx.button("Fetch Users", on_click=AppState.fetch_user_count), + spacing="4", + align="center", + ) +``` + +Backend variables are useful for storing sensitive data, database connections, or other server-side state that shouldn't be exposed to the client. + +## Computed Variables in Mixins + +Computed variables in mixins work the same as in regular State classes: + +```python demo exec +class FormattingMixin(rx.State, mixin=True): + value: float = 0.0 + + @rx.var + def formatted_value(self) -> str: + return f"${self.value:.2f}" + + @rx.var + def is_positive(self) -> bool: + return self.value > 0 + +class PriceState(FormattingMixin, rx.State): + product_name: str = "Widget" + + @rx.event + def set_price(self, price: str): + try: + self.value = float(price) + except ValueError: + self.value = 0.0 + +def formatting_example(): + return rx.vstack( + rx.heading(f"Product: {PriceState.product_name}"), + rx.text(f"Price: {PriceState.formatted_value}"), + rx.text(f"Positive: {PriceState.is_positive}"), + rx.input( + placeholder="Enter price", + on_blur=PriceState.set_price, + ), + spacing="4", + align="center", + ) +``` + +## Nested Mixin Inheritance + +Mixins can inherit from other mixins to create hierarchical functionality: + +```python demo exec +class BaseMixin(rx.State, mixin=True): + base_value: str = "base" + +class ExtendedMixin(BaseMixin, mixin=True): + extended_value: str = "extended" + + @rx.var + def combined_value(self) -> str: + return f"{self.base_value}-{self.extended_value}" + +class FinalState(ExtendedMixin, rx.State): + final_value: str = "final" + +def nested_mixin_example(): + return rx.vstack( + rx.text(f"Base: {FinalState.base_value}"), + rx.text(f"Extended: {FinalState.extended_value}"), + rx.text(f"Combined: {FinalState.combined_value}"), + rx.text(f"Final: {FinalState.final_value}"), + spacing="4", + align="center", + ) +``` + +This pattern allows you to build complex functionality by composing simpler mixins. + +## Best Practices + +```md alert info +# Mixin Design Guidelines + +- **Single Responsibility**: Each mixin should have a focused purpose +- **Avoid Deep Inheritance**: Keep mixin hierarchies shallow for clarity +- **Document Dependencies**: If mixins depend on specific variables, document them +- **Test Mixins**: Create test cases for mixin functionality +- **Naming Convention**: Use descriptive names ending with "Mixin" +``` + +## Limitations + +```md alert warning +# Important Limitations + +- Mixins cannot be instantiated directly - they must be inherited by concrete State classes +- Variable name conflicts between mixins are resolved by method resolution order (MRO) +- Mixins cannot override methods from the base State class +- The `mixin=True` parameter is required when defining a mixin +``` + +## Common Use Cases + +State mixins are particularly useful for: + +- **Form Validation**: Shared validation logic across forms +- **UI State Management**: Common modal, loading, or notification patterns +- **Logging**: Centralized logging and debugging +- **API Integration**: Shared HTTP client functionality +- **Data Formatting**: Consistent data presentation across components + +```python demo exec +class ValidationMixin(rx.State, mixin=True): + errors: dict[str, str] = {} + is_loading: bool = False + + @rx.event + def validate_email(self, email: str) -> bool: + if "@" not in email or "." not in email: + self.errors["email"] = "Invalid email format" + return False + self.errors.pop("email", None) + return True + + @rx.event + def validate_required(self, field: str, value: str) -> bool: + if not value.strip(): + self.errors[field] = f"{field.title()} is required" + return False + self.errors.pop(field, None) + return True + + @rx.event + def clear_errors(self): + self.errors = {} + +class ContactFormState(ValidationMixin, rx.State): + name: str = "" + email: str = "" + message: str = "" + + def set_name(self, value: str): + self.name = value + + def set_email(self, value: str): + self.email = value + + def set_message(self, value: str): + self.message = value + + @rx.event + def submit_form(self): + self.clear_errors() + valid_name = self.validate_required("name", self.name) + valid_email = self.validate_email(self.email) + valid_message = self.validate_required("message", self.message) + + if valid_name and valid_email and valid_message: + self.is_loading = True + yield rx.sleep(1) + self.is_loading = False + self.name = "" + self.email = "" + self.message = "" + +def validation_example(): + return rx.vstack( + rx.heading("Contact Form"), + rx.input( + placeholder="Name", + value=ContactFormState.name, + on_change=ContactFormState.set_name, + ), + rx.cond( + ContactFormState.errors.contains("name"), + rx.text(ContactFormState.errors["name"], color="red"), + ), + rx.input( + placeholder="Email", + value=ContactFormState.email, + on_change=ContactFormState.set_email, + ), + rx.cond( + ContactFormState.errors.contains("email"), + rx.text(ContactFormState.errors["email"], color="red"), + ), + rx.text_area( + placeholder="Message", + value=ContactFormState.message, + on_change=ContactFormState.set_message, + ), + rx.cond( + ContactFormState.errors.contains("message"), + rx.text(ContactFormState.errors["message"], color="red"), + ), + rx.button( + "Submit", + on_click=ContactFormState.submit_form, + loading=ContactFormState.is_loading, + ), + spacing="4", + align="center", + width="300px", + ) +``` + +By using state mixins, you can create modular, reusable state logic that keeps your application organized and reduces code duplication. diff --git a/docs/state_structure/overview.md b/docs/state_structure/overview.md new file mode 100644 index 00000000000..53060cbc907 --- /dev/null +++ b/docs/state_structure/overview.md @@ -0,0 +1,235 @@ +```python exec +import reflex as rx +from typing import Any +``` + +# Substates + +Substates allow you to break up your state into multiple classes to make it more manageable. This is useful as your app +grows, as it allows you to think about each page as a separate entity. Substates also allow you to share common state +resources, such as variables or event handlers. + +When a particular state class becomes too large, breaking it up into several substates can bring performance +benefits by only loading parts of the state that are used to handle a certain event. + +## Multiple States + +One common pattern is to create a substate for each page in your app. +This allows you to think about each page as a separate entity, and makes it easier to manage your code as your app grows. + +To create a substate, simply inherit from `rx.State` multiple times: + +```python +# index.py +import reflex as rx + +class IndexState(rx.State): + """Define your main state here.""" + data: str = "Hello World" + + +@rx.page() +def index(): + return rx.box(rx.text(IndexState.data)) + +# signup.py +import reflex as rx + + +class SignupState(rx.State): + """Define your signup state here.""" + username: str = "" + password: str = "" + + def signup(self): + ... + + +@rx.page() +def signup_page(): + return rx.box( + rx.input(value=SignupState.username), + rx.input(value=SignupState.password), + ) + +# login.py +import reflex as rx + +class LoginState(rx.State): + """Define your login state here.""" + username: str = "" + password: str = "" + + def login(self): + ... + +@rx.page() +def login_page(): + return rx.box( + rx.input(value=LoginState.username), + rx.input(value=LoginState.password), + ) +``` + +Separating the states is purely a matter of organization. You can still access the state from other pages by importing the state class. + +```python +# index.py + +import reflex as rx + +from signup import SignupState + +... + +def index(): + return rx.box( + rx.text(IndexState.data), + rx.input(value=SignupState.username), + rx.input(value=SignupState.password), + ) +``` + +## Accessing Arbitrary States + +An event handler in a particular state can access and modify vars in another state instance by calling +the `get_state` async method and passing the desired state class. If the requested state is not already loaded, +it will be loaded and deserialized on demand. + +In the following example, the `GreeterState` accesses the `SettingsState` to get the `salutation` and uses it +to update the `message` var. + +Notably, the widget that sets the salutation does NOT have to load the `GreeterState` when handling the +input `on_change` event, which improves performance. + +```python demo exec +class SettingsState(rx.State): + salutation: str = "Hello" + + def set_salutation(self, value: str): + self.salutation = value + +def set_salutation_popover(): + return rx.popover.root( + rx.popover.trigger( + rx.icon_button(rx.icon("settings")), + ), + rx.popover.content( + rx.input( + value=SettingsState.salutation, + on_change=SettingsState.set_salutation + ), + ), + ) + + +class GreeterState(rx.State): + message: str = "" + + @rx.event + async def handle_submit(self, form_data: dict[str, Any]): + settings = await self.get_state(SettingsState) + self.message = f"{settings.salutation} {form_data['name']}" + + +def index(): + return rx.vstack( + rx.form( + rx.vstack( + rx.hstack( + rx.input(placeholder="Name", id="name"), + set_salutation_popover(), + ), + rx.button("Submit"), + ), + reset_on_submit=True, + on_submit=GreeterState.handle_submit, + ), + rx.text(GreeterState.message), + ) +``` + +### Accessing Individual Var Values + +In addition to accessing entire state instances with `get_state`, you can retrieve individual variable values using the `get_var_value` method: + +```python +# Access a var value from another state +value = await self.get_var_value(OtherState.some_var) +``` + +This async method is particularly useful when you only need a specific value rather than loading the entire state. Using `get_var_value` can be more efficient than `get_state` when: + +1. You only need to access a single variable from another state +2. The other state contains a large amount of data +3. You want to avoid loading unnecessary data into memory + +Here's an example that demonstrates how to use `get_var_value` to access data between states: + +```python demo exec +# Define a state that holds a counter value +class CounterState(rx.State): + # This variable will be accessed from another state + count: int = 0 + + @rx.event + async def increment(self): + # Increment the counter when the button is clicked + self.count += 1 + +# Define a separate state that will display information +class DisplayState(rx.State): + # This will show the current count value + message: str = "" + + @rx.event + async def show_count(self): + # Use get_var_value to access just the count variable from CounterState + # This is more efficient than loading the entire state with get_state + current = await self.get_var_value(CounterState.count) + self.message = f"Current count: {current}" + +def var_value_example(): + return rx.vstack( + rx.heading("Get Var Value Example"), + rx.hstack( + # This button calls DisplayState.show_count to display the current count + rx.button("Get Count Value", on_click=DisplayState.show_count), + # This button calls CounterState.increment to increase the counter + rx.button("Increment", on_click=CounterState.increment), + ), + # Display the message from DisplayState + rx.text(DisplayState.message), + width="100%", + align="center", + spacing="4", + ) +``` + +In this example: + +1. We have two separate states: `CounterState` which manages a counter, and `DisplayState` which displays information +2. When you click "Increment", it calls `CounterState.increment()` to increase the counter value +3. When you click "Show Count", it calls `DisplayState.show_count()` which uses `get_var_value` to retrieve just the count value from `CounterState` without loading the entire state +4. The current count is then displayed in the message + +This pattern is useful when you have multiple states that need to interact with each other but don't need to access all of each other's data. + +If the var is not retrievable, `get_var_value` will raise an `UnretrievableVarValueError`. + +## Performance Implications + +When an event handler is called, Reflex will load the data not only for the substate containing +the event handler, but also all of its substates and parent states as well. +If a state has a large number of substates or contains a large amount of data, it can slow down processing +of events associated with that state. + +For optimal performance, keep a flat structure with most substate classes directly inheriting from `rx.State`. +Only inherit from another state when the parent holds data that is commonly used by the substate. +Implementing different parts of the app with separate, unconnected states ensures that only the necessary +data is loaded for processing events for a particular page or component. + +Avoid defining computed vars inside a state that contains a large amount of data, as +states with computed vars are always loaded to ensure the values are recalculated. +When using computed vars, it better to define them in a state that directly inherits from `rx.State` and +does not have other states inheriting from it, to avoid loading unnecessary data. diff --git a/docs/state_structure/shared_state.md b/docs/state_structure/shared_state.md new file mode 100644 index 00000000000..4666b16f4d1 --- /dev/null +++ b/docs/state_structure/shared_state.md @@ -0,0 +1,216 @@ +```python exec +import reflex as rx +from pcweb.templates.docpage import definition +``` + +# Shared State + +_New in version 0.8.23_. + +Defining a subclass of `rx.SharedState` creates a special type of state that may be shared by multiple clients. Shared State is useful for creating real-time collaborative applications where multiple users need to see and interact with the same data simultaneously. + +## Using SharedState + +An `rx.SharedState` subclass behaves similarly to a normal `rx.State` subclass and will be private to each client until it is explicitly linked to a given token. Once linked, any changes made to the Shared State by one client will be propagated to all other clients sharing the same token. + +```md alert info +# What should be used as a token? + +A token can be any string that uniquely identifies a group of clients that should share the same state. Common choices include room IDs, document IDs, or user group IDs. Ensure that the token is securely generated and managed to prevent unauthorized access to shared state. +``` + +```md alert warning +# Linked token cannot contain underscore (\_) characters. + +Underscore characters are currently used as an internal delimiter for tokens and will raise an exception if used for linked states. + +This is a temporary restriction and will be removed in a future release. +``` + +### Linking Shared State + +An `rx.SharedState` subclass can be linked to a token using the `_link_to` method, which is async and returns the linked state instance. After linking, subsequent events triggered against the shared state will be executed in the context of the linked state. To unlink from the token, return the result of awaiting the `_unlink` method. + +To try out the collaborative counter example, open this page in a second or third browser tab and click the "Link" button. You should see the count increment in all tabs when you click the "Increment" button in any of them. + +```python demo exec +class CollaborativeCounter(rx.SharedState): + count: int = 0 + + @rx.event + async def toggle_link(self): + if self._linked_to: + return await self._unlink() + else: + linked_state = await self._link_to("shared-global-counter") + linked_state.count += 1 # Increment count on link + + @rx.var + def is_linked(self) -> bool: + return bool(self._linked_to) + +def shared_state_example(): + return rx.vstack( + rx.text(f"Collaborative Count: {CollaborativeCounter.count}"), + rx.cond( + CollaborativeCounter.is_linked, + rx.button("Unlink", on_click=CollaborativeCounter.toggle_link), + rx.button("Link", on_click=CollaborativeCounter.toggle_link), + ), + rx.button("Increment", on_click=CollaborativeCounter.set_count(CollaborativeCounter.count + 1)), + ) +``` + +```md alert info +# Computed vars may reference SharedState + +Computed vars in other states may reference shared state data using `get_state`, just like private states. This allows private states to provide personalized views of shared data. + +Whenever the shared state is updated, any computed vars depending on it will be re-evaluated in the context of each client's private state. +``` + +### Identifying Clients + +Each client linked to a shared state can be uniquely identified by their `self.router.session.client_token`. Shared state events should _never_ rely on identifiers passed in as parameters, as these can be spoofed from the client. Instead, always use the `client_token` to identify the client triggering the event. + +```python demo exec +import uuid + +class SharedRoom(rx.SharedState): + shared_room: str = rx.LocalStorage() + _users: dict[str, str] = {} + + @rx.var + def user_list(self) -> str: + return ", ".join(self._users.values()) + + @rx.event + async def join(self, username: str): + if not self.shared_room: + self.shared_room = f"shared-room-{uuid.uuid4()}" + linked_state = await self._link_to(self.shared_room) + linked_state._users[self.router.session.client_token] = username + + @rx.event + async def leave(self): + if self._linked_to: + return await self._unlink() + + +class PrivateState(rx.State): + @rx.event + def handle_submit(self, form_data: dict): + return SharedRoom.join(form_data["username"]) + + @rx.var + async def user_in_room(self) -> bool: + shared_state = await self.get_state(SharedRoom) + return self.router.session.client_token in shared_state._users + + +def shared_room_example(): + return rx.vstack( + rx.text("Shared Room"), + rx.text(f"Users: {SharedRoom.user_list}"), + rx.cond( + PrivateState.user_in_room, + rx.button("Leave Room", on_click=SharedRoom.leave), + rx.form( + rx.input(placeholder="Enter your name", name="username"), + rx.button("Join Room"), + on_submit=PrivateState.handle_submit, + ), + ), + ) +``` + +```md alert warning +# Store sensitive data in backend-only vars with an underscore prefix + +Shared State data is synchronized to all linked clients, so avoid storing sensitive information (e.g., client_tokens, user credentials, personal data) in frontend vars, which would expose them to all users and allow them to be modified outside of explicit event handlers. Instead, use backend-only vars (prefixed with an underscore) to keep sensitive data secure on the server side and provide controlled access through event handlers and computed vars. +``` + +### Introspecting Linked Clients + +An `rx.SharedState` subclass has two attributes for determining link status and peers, which are updated during linking and unlinking, and come with some caveats. + +**`_linked_to: str`** + +Provides the token that the state is currently linked to, or empty string if not linked. + +This attribute is only set on the linked state instance returned by `_link_to`. It will be an empty string on any unlinked shared state instances. However, if another state links to a client's private token, then the `_linked_to` attribute will be set to the client's token rather than an empty string. + +When `_linked_to` equals `self.router.session.client_token`, it is assumed that the current client is unlinked, but another client has linked to this client's private state. Although this is possible, it is generally discouraged to link shared states to private client tokens. + +**`_linked_from: set[str]`** + +A set of client tokens that are currently linked to this shared state instance. + +This attribute is only updated during `_link_to` and `_unlink` calls. In situations where unlinking occurs otherwise, such as client disconnects, `self.reset()` is called, or state expires on the backend, `_linked_from` may contain stale client tokens that are no longer linked. These can be cleaned periodically by checking if the tokens still exist in `app.event_namespace.token_to_sid`. + +## Guidelines and Best Practices + +### Keep Shared State Minimal + +When defining a shared state, aim to keep it as minimal as possible. Only include the data and methods that need to be shared between clients. This helps reduce complexity and potential synchronization issues. + +Linked states are always loaded into the tree for each event on each linked client and large states take longer to serialize and transmit over the network. Because linked states are regularly loaded in the context of many clients, they incur higher lock contention, so minimizing loading time also reduces lock waiting time for other clients. + +### Prefer Backend-Only Vars in Shared State + +A shared state should primarily use backend-only vars (prefixed with an underscore) to store shared data. Often, not all users of the shared state need visibility into all of the data in the shared state. Use computed vars to provide sanitized access to shared data as needed. + +```python +from typing import Literal + +class SharedGameState(rx.SharedState): + # Sensitive user metadata stored in backend-only variable. + _players: dict[str, Literal["X", "O"]] = {} + + @rx.event + def make_move(self, x: int, y: int): + # Identify users by client_token, never by arguments passed to the event. + player_token = self.router.session.client_token + player_piece = self._players.get(player_token) +``` + +```md alert warning +# Do Not Trust Event Handler Arguments + +The client can send whatever data it wants to event handlers, so never rely on arguments passed to event handlers for sensitive information such as user identity or permissions. Always use secure identifiers like `self.router.session.client_token` to identify the client triggering the event. +``` + +### Expose Per-User Data via Private States + +If certain data in the shared state needs to be personalized for each user, prefer to expose that data through computed vars defined in private states. This allows each user to have their own view of the shared data without exposing sensitive information to other users. It also reduces the amount of unrelated data sent to each client and improves caching performance by keeping each user's view cached in their own private state, rather than always recomputing the shared state vars for each user that needs to have their information updated. + +Use async computed vars with `get_state` to access shared state data from private states. + +```python +class UserGameState(rx.State): + @rx.var + async def player_piece(self) -> str | None: + shared_state = await self.get_state(SharedGameState) + return shared_state._players.get(self.router.session.client_token) +``` + +### Use Dynamic Routes for Linked Tokens + +It is often convenient to define dynamic routes that include the linked token as part of the URL path. This allows users to easily share links to specific shared state instances. The dynamic route can use `on_load` to link the shared state to the token extracted from the URL. + +```python +class SharedRoom(rx.SharedState): + async def on_load(self): + # `self.room_id` is the automatically defined dynamic route var. + await self._link_to(self.room_id.replace("_", "-") or "default-room") + + +def room_page(): ... + + +app.add_route( + room_page, + path="/room/[room_id]", + on_load=SharedRoom.on_load, +) +``` diff --git a/docs/styling/common-props.md b/docs/styling/common-props.md new file mode 100644 index 00000000000..75491d265c9 --- /dev/null +++ b/docs/styling/common-props.md @@ -0,0 +1,227 @@ +# Style and Layout Props + +```python exec +import reflex as rx +from pcweb.styles.styles import get_code_style, cell_style +from pcweb.styles.colors import c_color + +props = { + "align": { + "description": "In a flex, it controls the alignment of items on the cross axis and in a grid layout, it controls the alignment of items on the block axis within their grid area (equivalent to align_items)", + "values": ["stretch", "center", "start", "end", "flex-start", "baseline"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/align-items", + }, + "backdrop_filter": { + "description": "Lets you apply graphical effects such as blurring or color shifting to the area behind an element", + "values": ["url(commonfilters.svg#filter)", "blur(2px)", "hue-rotate(120deg)", "drop-shadow(4px 4px 10px blue)"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter", + }, + "background": { + "description": "Sets all background style properties at once, such as color, image, origin and size, or repeat method (equivalent to bg)", + "values": ["green", "radial-gradient(crimson, skyblue)", "no-repeat url('../lizard.png')"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/background", + }, + "background_color": { + "description": "Sets the background color of an element", + "values": ["brown", "rgb(255, 255, 128)", "#7499ee"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/background-color", + }, + "background_image": { + "description": "Sets one or more background images on an element", + "values": ["url('../lizard.png')", "linear-gradient(#e66465, #9198e5)"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/background-image", + }, + "border": { + "description": "Sets an element's border, which sets the values of border_width, border_style, and border_color.", + "values": ["solid", "dashed red", "thick double #32a1ce", "4mm ridge rgba(211, 220, 50, .6)"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/border", + }, + "border_top / border_bottom / border_right / border_left": { + "description": "Sets an element's top / bottom / right / left border. It sets the values of border-(top / bottom / right / left)-width, border-(top / bottom / right / left)-style and border-(top / bottom / right / left)-color", + "values": ["solid", "dashed red", "thick double #32a1ce", "4mm ridge rgba(211, 220, 50, .6)"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/border-bottom", + }, + "border_color": { + "description": "Sets the color of an element's border (each side can be set individually using border_top_color, border_right_color, border_bottom_color, and border_left_color)", + "values": ["red", "red #32a1ce", "red rgba(170, 50, 220, .6) green", "red yellow green transparent"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/border-color", + }, + "border_radius": { + "description": "Rounds the corners of an element's outer border edge and you can set a single radius to make circular corners, or two radii to make elliptical corners", + "values": ["30px", "25% 10%", "10% 30% 50% 70%", "10% / 50%"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius", + }, + "border_width": { + "description": "Sets the width of an element's border", + "values": ["thick", "1em", "4px 1.25em", "0 4px 8px 12px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/border-width", + }, + "box_shadow": { + "description": "Adds shadow effects around an element's frame. You can set multiple effects separated by commas. A box shadow is described by X and Y offsets relative to the element, blur and spread radius, and color", + "values": ["10px 5px 5px red", "60px -16px teal", "12px 12px 2px 1px rgba(0, 0, 255, .2)", "3px 3px red, -1em 0 .4em olive;"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow", + }, + + "color": { + "description": "Sets the foreground color value of an element's text", + "values": ["rebeccapurple", "rgb(255, 255, 128)", "#00a400"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/color", + }, + "display": { + "description": "Sets whether an element is treated as a block or inline box and the layout used for its children, such as flow layout, grid or flex", + "values": ["block", "inline", "inline-block", "flex", "inline-flex", "grid", "inline-grid", "flow-root"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/display", + }, + "flex_grow": { + "description": " Sets the flex grow factor, which specifies how much of the flex container's remaining space should be assigned to the flex item's main size", + "values": ["1", "2", "3"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/flex-grow", + }, + "height": { + "description": "Sets an element's height", + "values": ["150px", "20em", "75%", "auto"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/height", + }, + "justify": { + "description": "Defines how the browser distributes space between and around content items along the main-axis of a flex container, and the inline axis of a grid container (equivalent to justify_content)", + "values": ["start", "center", "flex-start", "space-between", "space-around", "space-evenly", "stretch"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content", + }, + "margin": { + "description": "Sets the margin area (creates extra space around an element) on all four sides of an element", + "values": ["1em", "5% 0", "10px 50px 20px", "10px 50px 20px 0"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/margin", + }, + "margin_x / margin_y": { + "description": "Sets the margin area (creates extra space around an element) along the x-axis / y-axis and a positive value places it farther from its neighbors, while a negative value places it closer", + "values": ["1em", "10%", "10px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/margin", + }, + "margin_top / margin_right / margin_bottom / margin_left ": { + "description": "Sets the margin area (creates extra space around an element) on the top / right / bottom / left of an element", + "values": ["1em", "10%", "10px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/margin-top", + }, + "max_height / min_height": { + "description": "Sets the maximum / minimum height of an element and prevents the used value of the height property from becoming larger / smaller than the value specified for max_height / min_height", + "values": ["150px", "7em", "75%"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/max-height", + }, + "max_width / min_width": { + "description": "Sets the maximum / minimum width of an element and prevents the used value of the width property from becoming larger / smaller than the value specified for max_width / min_width", + "values": ["150px", "20em", "75%"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/max-width", + }, + "padding": { + "description": "Sets the padding area (creates extra space within an element) on all four sides of an element at once", + "values": ["1em", "10px 50px 30px 0", "0", "10px 50px 20px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/padding", + }, + "padding_x / padding_y": { + "description": "Creates extra space within an element along the x-axis / y-axis", + "values": ["1em", "10%", "10px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/padding", + }, + "padding_top / padding_right / padding_bottom / padding_left ": { + "description": "Sets the height of the padding area on the top / right / bottom / left of an element", + "values": ["1em", "10%", "20px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/padding-top", + }, + "position": { + "description": "Sets how an element is positioned in a document and the top, right, bottom, and left properties determine the final location of positioned elements", + "values": ["static", "relative", "absolute", "fixed", "sticky"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/position", + }, + "text_align": { + "description": "Sets the horizontal alignment of the inline-level content inside a block element or table-cell box", + "values": ["start", "end", "center", "justify", "left", "right"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/text-align", + }, + "text_wrap": { + "description": "Controls how text inside an element is wrapped", + "values": ["wrap", "nowrap", "balance", "pretty"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/text-wrap", + }, + "top / bottom / right / left": { + "description": "Sets the vertical / horizontal position of a positioned element. It does not effect non-positioned elements.", + "values": ["0", "4em", "10%", "20px"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/top", + }, + "width": { + "description": "Sets an element's width", + "values": ["150px", "20em", "75%", "auto"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/width", + }, + "white_space": { + "description": "Sets how white space inside an element is handled", + "values": ["normal", "nowrap", "pre", "break-spaces"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/white-space", + }, + "word_break": { + "description": "Sets whether line breaks appear wherever the text would otherwise overflow its content box", + "values": ["normal", "break-all", "keep-all", "break-word"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/word-break", + }, + "z_index": { + "description": "Sets the z-order of a positioned element and its descendants or flex and grid items, and overlapping elements with a larger z-index cover those with a smaller one", + "values": ["auto", "1", "5", "200"], + "link": "https://developer.mozilla.org/en-US/docs/Web/CSS/z-index", + }, + + +} + + +def show_props(key, props_dict): + prop_details = props_dict[key] + return rx.table.row( + rx.table.cell( + rx.link( + rx.hstack( + rx.code(key, style=get_code_style("violet")), + rx.icon("square_arrow_out_up_right", color=c_color("slate", 9), size=15, flex_shrink="0"), + align="center" + ), + href=prop_details["link"], + is_external=True, + ), + justify="start",), + rx.table.cell(prop_details["description"], justify="start", style=cell_style), + rx.table.cell(rx.hstack(*[rx.code(value, style=get_code_style("violet")) for value in prop_details["values"]], flex_wrap="wrap"), justify="start",), + justify="center", + align="center", + + ) + +``` + +Any [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS) prop can be used in a component in Reflex. This is a short list of the most commonly used props. To see all CSS props that can be used check out this [documentation](https://developer.mozilla.org/en-US/docs/Web/CSS). + +Hyphens in CSS property names may be replaced by underscores to use as valid python identifiers, i.e. the CSS prop `z-index` would be used as `z_index` in Reflex. + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell( + "Prop", justify="center" + ), + rx.table.column_header_cell( + "Description", + justify="center", + + ), + rx.table.column_header_cell( + "Potential Values", + justify="center", + ), + ) + ), + rx.table.body( + *[show_props(key, props) for key in props] + ), + width="100%", + padding_x="0", + size="1", +) +``` diff --git a/docs/styling/custom-stylesheets.md b/docs/styling/custom-stylesheets.md new file mode 100644 index 00000000000..01213c6d27d --- /dev/null +++ b/docs/styling/custom-stylesheets.md @@ -0,0 +1,191 @@ +```python exec +import reflex as rx +from pcweb.pages.docs import assets +``` + +# Custom Stylesheets + +Reflex allows you to add custom stylesheets. Simply pass the URLs of the stylesheets to `rx.App`: + +```python +app = rx.App( + stylesheets=[ + "https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css", + ], +) +``` + +## Local Stylesheets + +You can also add local stylesheets. Just put the stylesheet under [`assets/`]({assets.upload_and_download_files.path}) and pass the path to the stylesheet to `rx.App`: + +```python +app = rx.App( + stylesheets=[ + "/styles.css", # This path is relative to assets/ + ], +) +``` + +```md alert warning +# Always use a leading slash (/) when referencing files in the assets directory. + +Without a leading slash the path is considered relative to the current page route and may +not work for routes containing more than one path component, like `/blog/my-cool-post`. +``` + +## Styling with CSS + +You can use CSS variables directly in your Reflex app by passing them alongside the appropriae props. Create a `style.css` file inside the `assets` folder with the following lines: + +```css +:root { + --primary-color: blue; + --accent-color: green; +} +``` + +Then, after referencing the CSS file within the `stylesheets` props of `rx.App`, you can access the CSS props directly like this + +```python +app = rx.App( + theme=rx.theme(appearance="light"), + stylesheets=["/style.css"], +) +app.add_page( + rx.center( + rx.text("CSS Variables!"), + width="100%", + height="100vh", + bg="var(--primary-color)", + ), + "/", +) +``` + +## SASS/SCSS Support + +Reflex supports SASS/SCSS stylesheets alongside regular CSS. This allows you to use more advanced styling features like variables, nesting, mixins, and more. + +### Using SASS/SCSS Files + +To use SASS/SCSS files in your Reflex app: + +1. Create a `.sass` or `.scss` file in your `assets` directory +2. Reference the file in your `rx.App` configuration just like you would with CSS files + +```python +app = rx.App( + stylesheets=[ + "/styles.scss", # This path is relative to assets/ + "/sass/main.sass", # You can organize files in subdirectories + ], +) +``` + +Reflex automatically detects the file extension and compiles these files to CSS using the `libsass` package. + +### Example SASS/SCSS File + +Here's an example of a SASS file (`assets/styles.scss`) that demonstrates some of the features: + +```scss +// Variables +$primary-color: #3498db; +$secondary-color: #2ecc71; +$padding: 16px; + +// Nesting +.container { + background-color: $primary-color; + padding: $padding; + + .button { + background-color: $secondary-color; + padding: $padding / 2; + + &:hover { + opacity: 0.8; + } + } +} + +// Mixins +@mixin flex-center { + display: flex; + justify-content: center; + align-items: center; +} + +.centered-box { + @include flex-center; + height: 100px; +} +``` + +### Dependency Requirement + +The `libsass` package is required for SASS/SCSS compilation. If it's not installed, Reflex will show an error message. You can install it with: + +```bash +pip install "libsass>=0.23.0" +``` + +This package is included in the default Reflex installation, so you typically don't need to install it separately. + +## Fonts + +You can take advantage of Reflex's support for custom stylesheets to add custom fonts to your app. + +In this example, we will use the [JetBrains Mono]({"https://fonts.google.com/specimen/JetBrains+Mono"}) font from Google Fonts. First, add the stylesheet with the font to your app. You can get this link from the "Get embed code" section of the Google font page. + +```python +app = rx.App( + stylesheets=[ + "https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap", + ], +) +``` + +Then you can use the font in your component by setting the `font_family` prop. + +```python demo +rx.text( + "Check out my font", + font_family="JetBrains Mono", + font_size="1.5em", +) +``` + +## Local Fonts + +By making use of the two previous points, we can also make a stylesheet that allow you to use a font hosted on your server. + +If your font is called `MyFont.otf`, copy it in `assets/fonts`. + +Now we have the font ready, let's create the stylesheet `myfont.css`. + +```css +@font-face { + font-family: MyFont; + src: url("/fonts/MyFont.otf") format("opentype"); +} + +@font-face { + font-family: MyFont; + font-weight: bold; + src: url("/fonts/MyFont.otf") format("opentype"); +} +``` + +Add the reference to your new Stylesheet in your App. + +```python +app = rx.App( + stylesheets=[ + "/fonts/myfont.css", # This path is relative to assets/ + ], +) +``` + +And that's it! You can now use `MyFont` like any other FontFamily to style your components. diff --git a/docs/styling/layout.md b/docs/styling/layout.md new file mode 100644 index 00000000000..e3e29f2c302 --- /dev/null +++ b/docs/styling/layout.md @@ -0,0 +1,152 @@ +```python exec +import reflex as rx +``` + +# Layout Components + +Layout components such as `rx.flex`, `rx.container`, `rx.box`, etc. are used to organize and structure the visual presentation of your application. This page gives a breakdown of when and how each of these components might be used. + +```md video https://youtube.com/embed/ITOZkzjtjUA?start=3311&end=3853 +# Video: Example of Laying Out the Main Content of a Page +``` + +## Box + +`rx.box` is a generic component that can apply any CSS style to its children. It's a building block that can be used to apply a specific layout or style property. + +**When to use:** Use `rx.box` when you need to apply specific styles or constraints to a part of your interface. + +```python demo +rx.box( + rx.box( + "CSS color", + background_color="red", + border_radius="2px", + width="50%", + margin="4px", + padding="4px", + ), + rx.box( + "Radix Color", + background_color=rx.color("tomato", 3), + border_radius="5px", + width="80%", + margin="12px", + padding="12px", + ), + text_align="center", + width="100%", +) +``` + +## Stack + +`rx.stack` is a layout component that arranges its children in a single column or row, depending on the direction. It’s useful for consistent spacing between elements. + +**When to use:** Use `rx.stack` when you need to lay out a series of components either vertically or horizontally with equal spacing. + +```python demo +rx.flex( + rx.stack( + rx.box( + "Example", + bg="orange", + border_radius="3px", + width="20%", + ), + rx.box( + "Example", + bg="lightblue", + border_radius="3px", + width="30%", + ), + flex_direction="row", + width="100%", + ), + rx.stack( + rx.box( + "Example", + bg="orange", + border_radius="3px", + width="20%", + ), + rx.box( + "Example", + bg="lightblue", + border_radius="3px", + width="30%", + ), + flex_direction="column", + width="100%", + ), + width="100%", +) +``` + +## Flex + +The `rx.flex` component is used to create a flexible box layout, inspired by [CSS Flexbox](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Flexbox). It's ideal for designing a layout where the size of the items can grow and shrink dynamically based on the available space. + +**When to use:** Use `rx.flex` when you need a responsive layout that adjusts the size and position of child components dynamically. + +```python demo +rx.flex( + rx.card("Card 1"), + rx.card("Card 2"), + rx.card("Card 3"), + spacing="2", + width="100%", +) +``` + +## Grid + +`rx.grid` components are used to create complex responsive layouts based on a grid system, similar to [CSS Grid Layout](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout). + +**When to use:** Use `rx.grid` when dealing with complex layouts that require rows and columns, especially when alignment and spacing among multiple axes are needed. + +```python demo +rx.grid( + rx.foreach( + rx.Var.range(12), + lambda i: rx.card(f"Card {i + 1}", height="10vh"), + ), + columns="3", + spacing="4", + width="100%", +) +``` + +## Container + +The `rx.container` component typically provides padding and fixes the maximum width of the content inside it, often used to center content on large screens. + +**When to use:** Use `rx.container` for wrapping your application’s content in a centered block with some padding. + +```python demo +rx.box( + rx.container( + rx.card( + "This content is constrained to a max width of 448px.", + width="100%", + ), + size="1", + ), + rx.container( + rx.card( + "This content is constrained to a max width of 688px.", + width="100%", + ), + size="2", + ), + rx.container( + rx.card( + "This content is constrained to a max width of 880px.", + width="100%", + ), + size="3", + ), + background_color="var(--gray-3)", + width="100%", +) +``` diff --git a/docs/styling/overview.md b/docs/styling/overview.md new file mode 100644 index 00000000000..b247d3ef11a --- /dev/null +++ b/docs/styling/overview.md @@ -0,0 +1,185 @@ +```python exec +import reflex as rx +from pcweb.pages.docs import styling, library +``` + +# Styling + +Reflex components can be styled using the full power of [CSS]({"https://www.w3schools.com/css/"}). + +There are three main ways to add style to your app and they take precedence in the following order: + +1. **Inline:** Styles applied to a single component instance. +2. **Component:** Styles applied to components of a specific type. +3. **Global:** Styles applied to all components. + +```md alert success +# Style keys can be any valid CSS property name. + +To be consistent with Python standards, you can specify keys in `snake_case`. +``` + +## Global Styles + +You can pass a style dictionary to your app to apply base styles to all components. + +For example, you can set the default font family and font size for your app here just once rather than having to set it on every component. + +```python +style = { + "font_family": "Comic Sans MS", + "font_size": "16px", +} + +app = rx.App(style=style) +``` + +## Component Styles + +In your style dictionary, you can also specify default styles for specific component types or arbitrary CSS classes and IDs. + +```python +style = { + # Set the selection highlight color globally. + "::selection": { + "background_color": accent_color, + }, + # Apply global css class styles. + ".some-css-class": { + "text_decoration": "underline", + }, + # Apply global css id styles. + "#special-input": \{"width": "20vw"}, + # Apply styles to specific components. + rx.text: { + "font_family": "Comic Sans MS", + }, + rx.divider: { + "margin_bottom": "1em", + "margin_top": "0.5em", + }, + rx.heading: { + "font_weight": "500", + }, + rx.code: { + "color": "green", + }, +} + +app = rx.App(style=style) +``` + +Using style dictionaries like this, you can easily create a consistent theme for your app. + +```md alert warning +# Watch out for underscores in class names and IDs + +Reflex automatically converts `snake_case` identifiers into `camelCase` format when applying styles. To ensure consistency, it is recommended to use the dash character or camelCase identifiers in your own class names and IDs. To style third-party libraries relying on underscore class names, an external stylesheet should be used. See [custom stylesheets]({styling.custom_stylesheets.path}) for how to reference external CSS files. +``` + +## Inline Styles + +Inline styles apply to a single component instance. They are passed in as regular props to the component. + +```python demo +rx.text( + "Hello World", + background_image="linear-gradient(271.68deg, #EE756A 0.75%, #756AEE 88.52%)", + background_clip="text", + font_weight="bold", + font_size="2em", +) +``` + +Children components inherit inline styles unless they are overridden by their own inline styles. + +```python demo +rx.box( + rx.hstack( + rx.button("Default Button"), + rx.button("Red Button", color="red"), + ), + color="blue", +) +``` + +### Style Prop + +Inline styles can also be set with a `style` prop. This is useful for reusing styles between multiple components. + +```python exec +text_style = { + "color": "green", + "font_family": "Comic Sans MS", + "font_size": "1.2em", + "font_weight": "bold", + "box_shadow": "rgba(240, 46, 170, 0.4) 5px 5px, rgba(240, 46, 170, 0.3) 10px 10px", +} +``` + +```python +text_style={text_style} +``` + +```python demo +rx.vstack( + rx.text("Hello", style=text_style), + rx.text("World", style=text_style), +) +``` + +```python exec +style1 = { + "color": "green", + "font_family": "Comic Sans MS", + "border_radius": "10px", + "background_color": "rgb(107,99,246)", +} +style2 = { + "color": "white", + "border": "5px solid #EE756A", + "padding": "10px", +} +``` + +```python +style1={style1} +style2={style2} +``` + +```python demo +rx.box( + "Multiple Styles", + style=[style1, style2], +) +``` + +The style dictionaries are applied in the order they are passed in. This means that styles defined later will override styles defined earlier. + +## Theming + +As of Reflex 'v0.4.0', you can now theme your Reflex web apps. To learn more checkout the [Theme docs]({styling.theming.path}). + +The `Theme` component is used to change the theme of the application. The `Theme` can be set directly in your rx.App. + +```python +app = rx.App( + theme=rx.theme( + appearance="light", has_background=True, radius="large", accent_color="teal" + ) +) +``` + +Additionally you can modify the theme of your app through using the `Theme Panel` component which can be found in the [Theme Panel docs]({library.other.theme.path}). + +## Special Styles + +We support all of Chakra UI's [pseudo styles]({"https://v2.chakra-ui.com/docs/styled-system/style-props#pseudo"}). + +Below is an example of text that changes color when you hover over it. + +```python demo +rx.box( + rx.text("Hover Me", _hover={"color": "red"}), +) +``` diff --git a/docs/styling/responsive.md b/docs/styling/responsive.md new file mode 100644 index 00000000000..ea7e5d2177c --- /dev/null +++ b/docs/styling/responsive.md @@ -0,0 +1,172 @@ +```python exec +import reflex as rx +from pcweb.styles.styles import get_code_style, cell_style +``` + +# Responsive + +Reflex apps can be made responsive to look good on mobile, tablet, and desktop. + +You can pass a list of values to any style property to specify its value on different screen sizes. + +```python demo +rx.text( + "Hello World", + color=["orange", "red", "purple", "blue", "green"], +) +``` + +The text will change color based on your screen size. If you are on desktop, try changing the size of your browser window to see the color change. + +_New in 0.5.6_ + +Responsive values can also be specified using `rx.breakpoints`. Each size maps to a corresponding key, the value of which will be applied when the screen size is greater than or equal to the named breakpoint. + +```python demo +rx.text( + "Hello World", + color=rx.breakpoints( + initial="orange", + sm="purple", + lg="green", + ), +) +``` + +Custom breakpoints in CSS units can be mapped to values per component using a dictionary instead of named parameters. + +```python +rx.text( + "Hello World", + color=rx.breakpoints({ + "0px": "orange", + "48em": "purple", + "80em": "green", + }), +) +``` + +For the Radix UI components' fields that supports responsive value, you can also use `rx.breakpoints` (note that custom breakpoints and list syntax aren't supported). + +```python demo +rx.grid( + rx.foreach( + list(range(6)), + lambda _: rx.box(bg_color="#a7db76", height="100px", width="100px") + ), + columns=rx.breakpoints( + initial="2", + sm="4", + lg="6" + ), + spacing="4" +) +``` + +## Setting Defaults + +The default breakpoints are shown below. + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Size"), + rx.table.column_header_cell("Width"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.cell(rx.code("initial", style=get_code_style("violet"))), + rx.table.cell("0px", style=cell_style), + ), + rx.table.row( + rx.table.cell(rx.code("xs", style=get_code_style("violet"))), + rx.table.cell("30em", style=cell_style), + ), + rx.table.row( + rx.table.cell(rx.code("sm", style=get_code_style("violet"))), + rx.table.cell("48em", style=cell_style), + ), + rx.table.row( + rx.table.cell(rx.code("md", style=get_code_style("violet"))), + rx.table.cell("62em", style=cell_style), + ), + rx.table.row( + rx.table.cell(rx.code("lg", style=get_code_style("violet"))), + rx.table.cell("80em", style=cell_style), + ), + rx.table.row( + rx.table.cell(rx.code("xl", style=get_code_style("violet"))), + rx.table.cell("96em", style=cell_style), + ), + ), + margin_bottom="1em", +) +``` + +You can customize them using the style property. + +```python +app = rx.App(style=\{"breakpoints": ["520px", "768px", "1024px", "1280px", "1640px"]\}) +``` + +## Showing Components Based on Display + +A common use case for responsive is to show different components based on the screen size. + +Reflex provides useful helper components for this. + +```python demo +rx.vstack( + rx.desktop_only( + rx.text("Desktop View"), + ), + rx.tablet_only( + rx.text("Tablet View"), + ), + rx.mobile_only( + rx.text("Mobile View"), + ), + rx.mobile_and_tablet( + rx.text("Visible on Mobile and Tablet"), + ), + rx.tablet_and_desktop( + rx.text("Visible on Desktop and Tablet"), + ), +) +``` + +## Specifying Display Breakpoints + +You can specify the breakpoints to use for the responsive components by using the `display` style property. + +```python demo +rx.vstack( + rx.text( + "Hello World", + color="green", + display=["none", "none", "none", "none", "flex"], + ), + rx.text( + "Hello World", + color="blue", + display=["none", "none", "none", "flex", "flex"], + ), + rx.text( + "Hello World", + color="red", + display=["none", "none", "flex", "flex", "flex"], + ), + rx.text( + "Hello World", + color="orange", + display=["none", "flex", "flex", "flex", "flex"], + ), + rx.text( + "Hello World", + color="yellow", + display=["flex", "flex", "flex", "flex", "flex"], + ), +) +``` diff --git a/docs/styling/tailwind.md b/docs/styling/tailwind.md new file mode 100644 index 00000000000..fcbcfa227e7 --- /dev/null +++ b/docs/styling/tailwind.md @@ -0,0 +1,259 @@ +```python exec +import reflex as rx +from pcweb.pages.docs import library + +``` + +# Tailwind + +Reflex supports [Tailwind CSS]({"https://tailwindcss.com/"}) through a plugin system that provides better control and supports multiple Tailwind versions. + +## Plugin-Based Configuration + +The recommended way to use Tailwind CSS is through the plugin system: + +```python +import reflex as rx + +config = rx.Config( + app_name="myapp", + plugins=[ + rx.plugins.TailwindV4Plugin(), + ], +) +``` + +You can customize the Tailwind configuration by passing a config dictionary to the plugin: + +```python +import reflex as rx + +tailwind_config = { + "plugins": ["@tailwindcss/typography"], + "theme": { + "extend": { + "colors": { + "primary": "#3b82f6", + "secondary": "#64748b", + } + } + }, +} + +config = rx.Config( + app_name="myapp", + plugins=[ + rx.plugins.TailwindV4Plugin(tailwind_config), + ], +) +``` + +```````md alert info +## Migration from Legacy Configuration + +If you're currently using the legacy `tailwind` configuration parameter, you should migrate to using the plugin system: + +**Old approach (legacy):** + +```python +config = rx.Config( + app_name="my_app", + tailwind={ + "plugins": ["@tailwindcss/typography"], + "theme": {"extend": {"colors": {"primary": "#3b82f6"}}}, + }, +) +``` +``````` + +**New approach (plugin-based):** + +```python +tailwind_config = { + "plugins": ["@tailwindcss/typography"], + "theme": {"extend": {"colors": {"primary": "#3b82f6"}}}, +} + +config = rx.Config( + app_name="my_app", + plugins=[ + rx.plugins.TailwindV4Plugin(tailwind_config), + ], +) +``` + +```` + +### Choosing Between Tailwind Versions + +Reflex supports both Tailwind CSS v3 and v4: + +- **TailwindV4Plugin**: The recommended choice for new projects. Includes the latest features and performance improvements and is used by default in new Reflex templates. +- **TailwindV3Plugin**: Still supported for existing projects. Use this if you need compatibility with older Tailwind configurations. + +```python +# For Tailwind CSS v4 (recommended for new projects) +config = rx.Config( + app_name="myapp", + plugins=[rx.plugins.TailwindV4Plugin()], +) + +# For Tailwind CSS v3 (existing projects) +config = rx.Config( + app_name="myapp", + plugins=[rx.plugins.TailwindV3Plugin()], +) +```` + +All Tailwind configuration options are supported. + +You can use any of the [utility classes]({"https://tailwindcss.com/docs/utility-first"}) under the `class_name` prop: + +```python demo +rx.box( + "Hello World", + class_name="text-4xl text-center text-blue-500", +) +``` + +## Disabling Tailwind + +To disable Tailwind in your project, simply don't include any Tailwind plugins in your configuration. This will prevent Tailwind styles from being applied to your application. + +## Custom theme + +You can integrate custom Tailwind themes within your Reflex app as well. The setup process is similar to the CSS Styling method mentioned above, with only a few minor variations. + +Begin by creating a CSS file inside your `assets` folder. Inside the CSS file, include the following Tailwind directives: + +```css +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: blue; + --foreground: green; +} + +.dark { + --background: darkblue; + --foreground: lightgreen; +} +``` + +We define a couple of custom CSS variables (`--background` and `--foreground`) that will be used throughout your app for styling. These variables can be dynamically updated based on the theme. + +Tailwind defaults to light mode, but to handle dark mode, you can define a separate set of CSS variables under the `.dark` class. + +Tailwind Directives (`@tailwind base`, `@tailwind components`, `@tailwind utilities`): These are essential Tailwind CSS imports that enable the default base styles, components, and utility classes. + +Next, you'll need to configure Tailwind in your `rxconfig.py` file to ensure that the Reflex app uses your custom Tailwind setup. + +```python +import reflex as rx + +tailwind_config = { + "plugins": ["@tailwindcss/typography"], + "theme": { + "extend": { + "colors": { + "background": "var(--background)", + "foreground": "var(--foreground)" + }, + } + }, +} + +config = rx.Config( + app_name="app", + plugins=[ + rx.plugins.TailwindV4Plugin(tailwind_config), + ], +) +``` + +In the theme section, we're extending the default Tailwind theme to include custom colors. Specifically, we're referencing the CSS variables (`--background` and `--foreground`) that were defined earlier in your CSS file. + +The `rx.Config` object is used to initialize and configure your Reflex app. Here, we're passing the `tailwind_config` dictionary to ensure Tailwind's custom setup is applied to the app. + +Finally, to apply your custom styles and Tailwind configuration, you need to reference the CSS file you created in your `assets` folder inside the `rx.App` setup. This will allow you to use the custom properties (variables) directly within your Tailwind classes. + +In your `app.py` (or main application file), make the following changes: + +```python +app = rx.App( + theme=rx.theme(appearance="light"), + stylesheets=["/style.css"], +) +app.add_page( + rx.center( + rx.text("Tailwind & Reflex!"), + class_name="bg-background w-full h-[100vh]", + ), + "/", +) +``` + +The `bg-background` class uses the `--background` variable (defined in the CSS file), which will be applied as the background color. + +## Dynamic Styling + +You can style a component based of a condition using `rx.cond` or `rx.match`. + +```python demo exec +class TailwindState(rx.State): + active = False + + @rx.event + def toggle_active(self): + self.active = not self.active + +def tailwind_demo(): + return rx.el.div( + rx.el.button( + "Click me", + on_click=TailwindState.toggle_active, + class_name=( + "px-4 py-2 text-white rounded-md", + rx.cond( + TailwindState.active, + "bg-red-500", + "bg-blue-500", + ), + ), + ), + ) +``` + +## Using Tailwind Classes from the State + +When using Tailwind with Reflex, it's important to understand that class names must be statically defined in your code for Tailwind to properly compile them. If you dynamically generate class names from state variables or functions at runtime, Tailwind won't be able to detect these classes during the build process, resulting in missing styles in your application. + +For example, this won't work correctly because the class names are defined in the state: + +```python demo exec +class TailwindState(rx.State): + active = False + + @rx.var + def button_class(self) -> str: + return "bg-accent" if self.active else "bg-secondary" + + @rx.event + def toggle_active(self): + self.active = not self.active + +def tailwind_demo(): + return rx.el.button( + f"Click me: {TailwindState.active}", + class_name=TailwindState.button_class, + on_click=TailwindState.toggle_active, + ) +``` + +## Using Tailwind with Reflex Core Components + +Reflex core components are built on Radix Themes, which means they come with pre-defined styling. When you apply Tailwind classes to these components, you may encounter styling conflicts or unexpected behavior as the Tailwind styles compete with the built-in Radix styles. + +For the best experience when using Tailwind CSS in your Reflex application, we recommend using the lower-level `rx.el` components. These components don't have pre-applied styles, giving you complete control over styling with Tailwind classes without any conflicts. Check the list of HTML components [here]({library.other.html.path}). diff --git a/docs/styling/theming.md b/docs/styling/theming.md new file mode 100644 index 00000000000..78493c5de7c --- /dev/null +++ b/docs/styling/theming.md @@ -0,0 +1,204 @@ +```python exec +import reflex as rx +from pcweb.constants import REFLEX_ASSETS_CDN +from pcweb.pages.docs import library +from pcweb.styles.styles import get_code_style_rdx, cell_style +``` + +# Theming + +As of Reflex `v0.4.0`, you can now theme your Reflex applications. The core of our theming system is directly based on the [Radix Themes](https://www.radix-ui.com) library. This allows you to easily change the theme of your application along with providing a default light and dark theme. Themes cause all the components to have a unified color appearance. + +## Overview + +The `Theme` component is used to change the theme of the application. The `Theme` can be set directly in your rx.App. + +```python +app = rx.App( + theme=rx.theme( + appearance="light", has_background=True, radius="large", accent_color="teal" + ) +) +``` + +Here are the props that can be passed to the `rx.theme` component: + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name", class_name="table-header"), + rx.table.column_header_cell("Type", class_name="table-header"), + rx.table.column_header_cell("Description", class_name="table-header"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell(rx.code("has_background", style=get_code_style_rdx("violet"), class_name="code-style")), + rx.table.cell(rx.code("Bool", style=get_code_style_rdx("gray"), class_name="code-style")), + rx.table.cell("Whether to apply the themes background color to the theme node. Defaults to True.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("appearance", style=get_code_style_rdx("violet"), class_name="code-style")), + rx.table.cell(rx.code('"inherit" | "light" | "dark"', style=get_code_style_rdx("gray"), class_name="code-style")), + rx.table.cell("The appearance of the theme. Can be 'light' or 'dark'. Defaults to 'light'.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("accent_color", style=get_code_style_rdx("violet"), class_name="code-style")), + rx.table.cell(rx.code("Str", style=get_code_style_rdx("gray"), class_name="code-style")), + rx.table.cell("The primary color used for default buttons, typography, backgrounds, etc.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("gray_color", style=get_code_style_rdx("violet"), class_name="code-style")), + rx.table.cell(rx.code("Str", style=get_code_style_rdx("gray"), class_name="code-style")), + rx.table.cell("The secondary color used for default buttons, typography, backgrounds, etc.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("panel_background", style=get_code_style_rdx("violet"), class_name="code-style")), + rx.table.cell(rx.code('"solid" | "translucent"', style=get_code_style_rdx("gray"), class_name="code-style")), + rx.table.cell('Whether panel backgrounds are translucent: "solid" | "translucent" (default).', style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("radius", style=get_code_style_rdx("violet"), class_name="code-style")), + rx.table.cell(rx.code('"none" | "small" | "medium" | "large" | "full"', style=get_code_style_rdx("gray"))), + rx.table.cell("The radius of the theme. Can be 'small', 'medium', or 'large'. Defaults to 'medium'.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("scaling", style=get_code_style_rdx("violet"), class_name="code-style")), + rx.table.cell(rx.code('"90%" | "95%" | "100%" | "105%" | "110%"', style=get_code_style_rdx("gray"), class_name="code-style")), + rx.table.cell("Scale of all theme items.", style=cell_style), + ), + ), + variant="surface", + margin_y="1em", +) + +``` + +Additionally you can modify the theme of your app through using the `Theme Panel` component which can be found in the [Theme Panel docs]({library.other.theme.path}). + +## Colors + +### Color Scheme + +On a high-level, component `color_scheme` inherits from the color specified in the theme. This means that if you change the theme, the color of the component will also change. Available colors can be found [here](https://www.radix-ui.com/colors). + +You can also specify the `color_scheme` prop. + +```python demo +rx.flex( + rx.button( + "Hello World", + color_scheme="tomato", + ), + rx.button( + "Hello World", + color_scheme="teal", + ), + spacing="2" +) +``` + +### Shades + +Sometime you may want to use a specific shade of a color from the theme. This is recommended vs using a hex color directly as it will automatically change when the theme changes appearance change from light/dark. + +To access a specific shade of color from the theme, you can use the `rx.color`. When switching to light and dark themes, the color will automatically change. Shades can be accessed by using the color name and the shade number. The shade number ranges from 1 to 12. Additionally, they can have their alpha value set by using the `True` parameter it defaults to `False`. A full list of colors can be found [here](https://www.radix-ui.com/colors). + +```python demo +rx.flex( + rx.button( + "Hello World", + color=rx.color("grass", 1), + background_color=rx.color("grass", 7), + border_color=f"1px solid {rx.color('grass', 1)}", + ), + spacing="2" +) +``` + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Type"), + rx.table.column_header_cell("Description"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell(rx.code("color", style=get_code_style_rdx("violet"), class_name="code-style")), + rx.table.cell(rx.code("Str", style=get_code_style_rdx("gray"), class_name="code-style")), + rx.table.cell("The color to use. Can be any valid accent color or 'accent' to reference the current theme color.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("shade", style=get_code_style_rdx("violet"), class_name="code-style")), + rx.table.cell(rx.link(rx.code('1 - 12', style=get_code_style_rdx("gray"), class_name="code-style"), href="https://www.radix-ui.com/colors")), + rx.table.cell("The shade of the color to use. Defaults to 7.", style=cell_style), + ), + rx.table.row( + rx.table.row_header_cell(rx.code("alpha", style=get_code_style_rdx("violet"), class_name="code-style")), + rx.table.cell(rx.code("Bool", style=get_code_style_rdx("gray"), class_name="code-style")), + rx.table.cell("Whether to use the alpha value of the color. Defaults to False.", style=cell_style), + ) + ), + variant="surface", + margin_y="1em", +) + +``` + +### Regular Colors + +You can also use standard hex, rgb, and rgba colors. + +```python demo +rx.flex( + rx.button( + "Hello World", + color="white", + background_color="#87CEFA", + border="1px solid rgb(176,196,222)", + ), + spacing="2" +) +``` + +## Toggle Appearance + +To toggle between the light and dark mode manually, you can use the `toggle_color_mode` with the desired event trigger of your choice. + +```python + +from reflex.style import toggle_color_mode + + + +def index(): + return rx.button( + "Toggle Color Mode", + on_click=toggle_color_mode, + ) +``` + +## Appearance Conditional Rendering + +To render a different component depending on whether the app is in `light` mode or `dark` mode, you can use the `rx.color_mode_cond` component. The first component will be rendered if the app is in `light` mode and the second component will be rendered if the app is in `dark` mode. + +```python demo +rx.color_mode_cond( + light=rx.image(src=f"{REFLEX_ASSETS_CDN}logos/light/reflex.svg", alt="Reflex Logo light", height="4em"), + dark=rx.image(src=f"{REFLEX_ASSETS_CDN}logos/dark/reflex.svg", alt="Reflex Logo dark", height="4em"), +) +``` + +This can also be applied to props. + +```python demo +rx.button( + "Hello World", + color=rx.color_mode_cond(light="black", dark="white"), + background_color=rx.color_mode_cond(light="white", dark="black"), +) +``` diff --git a/docs/tr/README.md b/docs/tr/README.md deleted file mode 100644 index be888117332..00000000000 --- a/docs/tr/README.md +++ /dev/null @@ -1,248 +0,0 @@ -
-Reflex Logo -
- -### **✨ Saf Python'da performanslı, özelleştirilebilir web uygulamaları. Saniyeler içinde dağıtın. ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) - -
- ---- - -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex, saf Python'da tam yığın web uygulamaları oluşturmak için bir kütüphanedir. - -Temel özellikler: - -- **Saf Python** - Uygulamanızın ön uç ve arka uç kısımlarının tamamını Python'da yazın, Javascript öğrenmenize gerek yok. -- **Tam Esneklik** - Reflex ile başlamak kolaydır, ancak karmaşık uygulamalara da ölçeklenebilir. -- **Anında Dağıtım** - Oluşturduktan sonra, uygulamanızı [tek bir komutla](https://reflex.dev/docs/hosting/deploy-quick-start/) dağıtın veya kendi sunucunuzda barındırın. - -Reflex'in perde arkasında nasıl çalıştığını öğrenmek için [mimari sayfamıza](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) göz atın. - -## ⚙️ Kurulum - -Bir terminal açın ve çalıştırın (Python 3.10+ gerekir): - -```bash -pip install reflex -``` - -## 🥳 İlk Uygulamanı Oluştur - -`reflex`'i kurduğunuzda `reflex` komut satırı aracınıda kurmuş olursunuz. - -Kurulumun başarılı olduğunu test etmek için yeni bir proje oluşturun. (`my_app_name`'i proje ismiyle değiştirin.): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -Bu komut ile birlikte yeni oluşturduğunuz dizinde bir şablon uygulaması oluşturur. - -Uygulamanızı geliştirme modunda başlatabilirsiniz: - -```bash -reflex run -``` - -Uygulamanızın http://localhost:3000 adresinde çalıştığını görmelisiniz. - -Şimdi `my_app_name/my_app_name.py` yolundaki kaynak kodu düzenleyebilirsiniz. Reflex'in hızlı yenileme özelliği vardır, böylece kodunuzu kaydettiğinizde değişikliklerinizi anında görebilirsiniz. - -## 🫧 Örnek Uygulama - -Bir örnek üzerinden gidelim: [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node) kullanarak bir görüntü oluşturma arayüzü oluşturalım. Basit olması açısından, yalnızca [OpenAI API](https://platform.openai.com/docs/api-reference/authentication)'ını kullanıyoruz, ancak bunu yerel olarak çalıştırılan bir ML modeliyle değiştirebilirsiniz. - -  - -
-A frontend wrapper for DALL·E, shown in the process of generating an image. -
- -  - -İşte bunu oluşturmak için kodun tamamı. Her şey sadece bir Python dosyasıyla hazırlandı! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """Uygulama durumu.""" - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Prompt'tan görüntüyü alın.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True - - -def index(): - return rx.center( - rx.vstack( - rx.heading("DALL-E", font_size="1.5em"), - rx.input( - placeholder="Enter a prompt..", - on_blur=State.set_prompt, - width="25em", - ), - rx.button( - "Generate Image", - on_click=State.get_image, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Sayfa ve durumu uygulamaya ekleyin. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## Daha Detaylı İceleyelim - -
-DALL-E uygulamasının arka uç ve ön uç kısımları arasındaki farkları açıklama. -
- -### **Reflex UI** - -UI (Kullanıcı Arayüzü) ile başlayalım. - -```python -def index(): - return rx.center( - ... - ) -``` - -Bu `index` fonkisyonu uygulamanın frontend'ini tanımlar. - -Frontend'i oluşturmak için `center`, `vstack`, `input`, ve `button` gibi farklı bileşenler kullanıyoruz. Karmaşık düzenler oluşturmak için bileşenleri birbirinin içine yerleştirilebiliriz. Ayrıca bunları CSS'nin tüm gücüyle şekillendirmek için anahtar kelime argümanları kullanabilirsiniz. - -Reflex, işinizi kolaylaştırmak için [60'tan fazla dahili bileşen](https://reflex.dev/docs/library) içerir. Aktif olarak yeni bileşen ekliyoruz ve [kendi bileşenlerinizi oluşturmak](https://reflex.dev/docs/wrapping-react/overview/) oldukça kolay. - -### **Durum (State)** - -Reflex arayüzünüzü durumunuzun bir fonksiyonu olarak temsil eder. - -```python -class State(rx.State): - """Uygulama durumu.""" - prompt = "" - image_url = "" - processing = False - complete = False -``` - -Durum (State), bir uygulamadaki değişebilen tüm değişkenleri (vars olarak adlandırılır) ve bunları değiştiren fonksiyonları tanımlar. - -Burada durum `prompt` ve `image_url`inden oluşur. Ayrıca düğmenin ne zaman devre dışı bırakılacağını (görüntü oluşturma sırasında) ve görüntünün ne zaman gösterileceğini belirtmek için `processing` ve `complete` booleanları da vardır. - -### **Olay İşleyicileri (Event Handlers)** - -```python -def get_image(self): - """Prompt'tan görüntüyü alın.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True -``` - -Durum içinde, durum değişkenlerini değiştiren olay işleyicileri adı verilen fonksiyonları tanımlarız. Olay işleyicileri, Reflex'te durumu değiştirebilmemizi sağlar. Bir düğmeye tıklamak veya bir metin kutusuna yazı yazmak gibi kullanıcı eylemlerine yanıt olarak çağrılabilirler. Bu eylemlere olay denir. - -DALL·E uygulamamız OpenAI API'ından bu görüntüyü almak için `get_image` adlı bir olay işleyicisine sahiptir. Bir olay işleyicisinin ortasında `yield`ın kullanılması UI'ın güncellenmesini sağlar. Aksi takdirde UI olay işleyicisinin sonunda güncellenecektir. - -### **Yönlendirme (Routing)** - -En sonunda uygulamamızı tanımlıyoruz. - -```python -app = rx.App() -``` - -Uygulamamızın kök dizinine index bileşeninden bir sayfa ekliyoruz. Ayrıca sayfa önizlemesinde/tarayıcı sekmesinde görünecek bir başlık ekliyoruz. - -```python -app.add_page(index, title="DALL-E") -``` - -Daha fazla sayfa ekleyerek çok sayfalı bir uygulama oluşturabilirsiniz. - -## 📑 Kaynaklar - -
- -📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Component Library](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Durum - -Reflex, Aralık 2022'de Pynecone adıyla piyasaya sürüldü. - -2025'in başından itibaren, Reflex uygulamaları için en iyi barındırma deneyimini sunmak amacıyla [Reflex Cloud](https://cloud.reflex.dev) hizmete girmiştir. Bunu geliştirmeye ve daha fazla özellik eklemeye devam edeceğiz. - -Reflex'in her hafta yeni sürümleri ve özellikleri geliyor! Güncel kalmak için bu depoyu :star: yıldızlamayı ve :eyes: izlediğinizden emin olun. - -## Katkı - -Her boyuttaki katkıları memnuniyetle karşılıyoruz! Aşağıda Reflex topluluğuna adım atmanın bazı yolları mevcut. - -- **Discord Kanalımıza Katılın**: [Discord'umuz](https://discord.gg/T5WSbC2YtQ), Reflex projeniz hakkında yardım almak ve nasıl katkıda bulunabileceğinizi tartışmak için en iyi yerdir. -- **GitHub Discussions**: Eklemek istediğiniz özellikler veya kafa karıştırıcı, açıklığa kavuşturulması gereken şeyler hakkında konuşmanın harika bir yolu. -- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues) hataları bildirmenin mükemmel bir yoludur. Ayrıca mevcut bir sorunu deneyip çözebilir ve bir PR (Pull Requests) gönderebilirsiniz. - -Beceri düzeyiniz veya deneyiminiz ne olursa olsun aktif olarak katkıda bulunacak kişiler arıyoruz. Katkı sağlamak için katkı sağlama rehberimize bakabilirsiniz: [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) - -## Hepsi Katkıda Bulunanlar Sayesinde: - - - - - -## Lisans - -Reflex açık kaynaklıdır ve [Apache License 2.0](/LICENSE) altında lisanslıdır. diff --git a/docs/ui/overview.md b/docs/ui/overview.md new file mode 100644 index 00000000000..f6b34f49037 --- /dev/null +++ b/docs/ui/overview.md @@ -0,0 +1,81 @@ +```python exec +from pcweb.pages.docs import components +from pcweb.pages.docs.library import library +import reflex as rx +``` + +# UI Overview + +Components are the building blocks for your app's user interface (UI). They are the visual elements that make up your app, like buttons, text, and images. + +## Component Basics + +Components are made up of children and props. + +```md definition +# Children + +- Text or other Reflex components nested inside a component. +- Passed as **positional arguments**. + +# Props + +- Attributes that affect the behavior and appearance of a component. +- Passed as **keyword arguments**. +``` + +Let's take a look at the `rx.text` component. + +```python demo +rx.text('Hello World!', color='blue', font_size="1.5em") +``` + +Here `"Hello World!"` is the child text to display, while `color` and `font_size` are props that modify the appearance of the text. + +```md alert success +# Regular Python data types can be passed in as children to components. This is useful for passing in text, numbers, and other simple data types. +``` + +## Another Example + +Now let's take a look at a more complex component, which has other components nested inside it. The `rx.vstack` component is a container that arranges its children vertically with space between them. + +```python demo +rx.vstack( + rx.heading("Sample Form"), + rx.input(placeholder="Name"), + rx.checkbox("Subscribe to Newsletter"), +) +``` + +Some props are specific to a component. For example, the `header` and `content` props of the `rx.accordion.item` component show the heading and accordion content details of the accordion respectively. + +Styling props like `color` are shared across many components. + +```md alert info +# You can find all the props for a component by checking its documentation page in the [component library]({library.path}). +``` + +## Pages + +Reflex apps are organized into pages, each of which maps to a different URL. + +Pages are defined as functions that return a component. By default, the function name will be used as the path, but you can also specify a route explicitly. + +```python +def index(): + return rx.text('Root Page') + + +def about(): + return rx.text('About Page') + + +app = rx.App() +app.add_page(index, route="/") +app.add_page(about, route="/about") +``` + +In this example we add a page called `index` at the root route. +If you `reflex run` the app, you will see the `index` page at `http://localhost:3000`. +Similarly, the `about` page will be available at `http://localhost:3000/about`. diff --git a/docs/utility_methods/exception_handlers.md b/docs/utility_methods/exception_handlers.md new file mode 100644 index 00000000000..b0b62f2f532 --- /dev/null +++ b/docs/utility_methods/exception_handlers.md @@ -0,0 +1,43 @@ +# Exception handlers + +_Added in v0.5.7_ + +Exceptions handlers are functions that can be assigned to your app to handle exceptions that occur during the application runtime. +They are useful for customizing the response when an error occurs, logging errors, and performing cleanup tasks. + +## Types + +Reflex support two type of exception handlers `frontend_exception_handler` and `backend_exception_handler`. + +They are used to handle exceptions that occur in the `frontend` and `backend` respectively. + +The `frontend` errors are coming from the JavaScript side of the application, while `backend` errors are coming from the the event handlers on the Python side. + +## Register an Exception Handler + +To register an exception handler, assign it to `app.frontend_exception_handler` or `app.backend_exception_handler` to assign a function that will handle the exception. + +The expected signature for an error handler is `def handler(exception: Exception)`. + +```md alert warning +# Only named functions are supported as exception handler. +``` + +## Examples + +```python +import reflex as rx + +def custom_frontend_handler(exception: Exception) -> None: + # My custom logic for frontend errors + print("Frontend Error: " + str(exception)) + +def custom_backend_handler(exception: Exception) -> Optional[rx.event.EventSpec]: + # My custom logic for backend errors + print("Backend Error: " + str(exception)) + +app = rx.App( + frontend_exception_handler = custom_frontend_handler, + backend_exception_handler = custom_backend_handler + ) +``` diff --git a/docs/utility_methods/lifespan_tasks.md b/docs/utility_methods/lifespan_tasks.md new file mode 100644 index 00000000000..bf5f84eb8ad --- /dev/null +++ b/docs/utility_methods/lifespan_tasks.md @@ -0,0 +1,84 @@ +# Lifespan Tasks + +_Added in v0.5.2_ + +Lifespan tasks are coroutines that run when the backend server is running. They +are useful for setting up the initial global state of the app, running periodic +tasks, and cleaning up resources when the server is shut down. + +Lifespan tasks are defined as async coroutines or async contextmanagers. To avoid +blocking the event thread, never use `time.sleep` or perform non-async I/O within +a lifespan task. + +In dev mode, lifespan tasks will stop and restart when a hot-reload occurs. + +## Tasks + +Any async coroutine can be used as a lifespan task. It will be started when the +backend comes up and will run until it returns or is cancelled due to server +shutdown. Long-running tasks should catch `asyncio.CancelledError` to perform +any necessary clean up. + +```python +async def long_running_task(foo, bar): + print(f"Starting \{foo} \{bar} task") + some_api = SomeApi(foo) + try: + while True: + updates = some_api.poll_for_updates() + other_api.push_changes(updates, bar) + await asyncio.sleep(5) # add some polling delay to avoid running too often + except asyncio.CancelledError: + some_api.close() # clean up the API if needed + print("Task was stopped") +``` + +### Register the Task + +To register a lifespan task, use `app.register_lifespan_task(coro_func, **kwargs)`. +Any keyword arguments specified during registration will be passed to the task. + +If the task accepts the special argument, `app`, it will be an instance of the `FastAPI` object +associated with the app. + +```python +app = rx.App() +app.register_lifespan_task(long_running_task, foo=42, bar=os.environ["BAR_PARAM"]) +``` + +## Context Managers + +Lifespan tasks can also be defined as async contextmanagers. This is useful for +setting up and tearing down resources and behaves similarly to the ASGI lifespan +protocol. + +Code up to the first `yield` will run when the backend comes up. As the backend +is shutting down, the code after the `yield` will run to clean up. + +Here is an example borrowed from the FastAPI docs and modified to work with this +interface. + +```python +from contextlib import asynccontextmanager + + +def fake_answer_to_everything_ml_model(x: float): + return x * 42 + + +ml_models = \{} + + +@asynccontextmanager +async def setup_model(app: FastAPI): + # Load the ML model + ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model + yield + # Clean up the ML models and release the resources + ml_models.clear() + +... + +app = rx.App() +app.register_lifespan_task(setup_model) +``` diff --git a/docs/utility_methods/other_methods.md b/docs/utility_methods/other_methods.md new file mode 100644 index 00000000000..e821faccf32 --- /dev/null +++ b/docs/utility_methods/other_methods.md @@ -0,0 +1,14 @@ +# Other Methods + +- `reset`: set all Vars to their default value for the given state (including substates). +- `get_value`: returns the value of a Var **without tracking changes to it**. This is useful + for serialization where the tracking wrapper is considered unserializable. +- `dict`: returns all state Vars (and substates) as a dictionary. This is + used internally when a page is first loaded and needs to be "hydrated" and + sent to the client. + +## Special Attributes + +- `dirty_vars`: a set of all Var names that have been modified since the last + time the state was sent to the client. This is used internally to determine + which Vars need to be sent to the client after processing an event. diff --git a/docs/utility_methods/router_attributes.md b/docs/utility_methods/router_attributes.md new file mode 100644 index 00000000000..fc00dbdb059 --- /dev/null +++ b/docs/utility_methods/router_attributes.md @@ -0,0 +1,116 @@ +```python exec box +import reflex as rx +from pcweb.styles.styles import get_code_style, cell_style + +class RouterState(rx.State): + pass + + +router_data = [ + {"name": "rx.State.router.page.host", "value": RouterState.router.page.host}, + {"name": "rx.State.router.page.path", "value": RouterState.router.page.path}, + {"name": "rx.State.router.page.raw_path", "value": RouterState.router.page.raw_path}, + {"name": "rx.State.router.page.full_path", "value": RouterState.router.page.full_path}, + {"name": "rx.State.router.page.full_raw_path", "value": RouterState.router.page.full_raw_path}, + {"name": "rx.State.router.page.params", "value": RouterState.router.page.params.to_string()}, + {"name": "rx.State.router.session.client_token", "value": RouterState.router.session.client_token}, + {"name": "rx.State.router.session.session_id", "value": RouterState.router.session.session_id}, + {"name": "rx.State.router.session.client_ip", "value": RouterState.router.session.client_ip}, + {"name": "rx.State.router.headers.host", "value": RouterState.router.headers.host}, + {"name": "rx.State.router.headers.origin", "value": RouterState.router.headers.origin}, + {"name": "rx.State.router.headers.upgrade", "value": RouterState.router.headers.upgrade}, + {"name": "rx.State.router.headers.connection", "value": RouterState.router.headers.connection}, + {"name": "rx.State.router.headers.cookie", "value": RouterState.router.headers.cookie}, + {"name": "rx.State.router.headers.pragma", "value": RouterState.router.headers.pragma}, + {"name": "rx.State.router.headers.cache_control", "value": RouterState.router.headers.cache_control}, + {"name": "rx.State.router.headers.user_agent", "value": RouterState.router.headers.user_agent}, + {"name": "rx.State.router.headers.sec_websocket_version", "value": RouterState.router.headers.sec_websocket_version}, + {"name": "rx.State.router.headers.sec_websocket_key", "value": RouterState.router.headers.sec_websocket_key}, + {"name": "rx.State.router.headers.sec_websocket_extensions", "value": RouterState.router.headers.sec_websocket_extensions}, + {"name": "rx.State.router.headers.accept_encoding", "value": RouterState.router.headers.accept_encoding}, + {"name": "rx.State.router.headers.accept_language", "value": RouterState.router.headers.accept_language}, + {"name": "rx.State.router.headers.raw_headers", "value": RouterState.router.headers.raw_headers.to_string()}, + ] + +``` + +# State Utility Methods + +The state object has several methods and attributes that return information +about the current page, session, or state. + +## Router Attributes + +The `self.router` attribute has several sub-attributes that provide various information: + +- `router.page`: data about the current page and route + - `host`: The hostname and port serving the current page (frontend). + - `path`: The path of the current page (for dynamic pages, this will contain the slug) + - `raw_path`: The path of the page displayed in the browser (including params and dynamic values) + - `full_path`: `path` with `host` prefixed + - `full_raw_path`: `raw_path` with `host` prefixed + - `params`: Dictionary of query params associated with the request + +- `router.session`: data about the current session + - `client_token`: UUID associated with the current tab's token. Each tab has a unique token. + - `session_id`: The ID associated with the client's websocket connection. Each tab has a unique session ID. + - `client_ip`: The IP address of the client. Many users may share the same IP address. + +- `router.headers`: headers associated with the websocket connection. These values can only change when the websocket is re-established (for example, during page refresh). + - `host`: The hostname and port serving the websocket (backend). + - `origin`: The origin of the request. + - `upgrade`: The upgrade header for websocket connections. + - `connection`: The connection header. + - `cookie`: The cookie header. + - `pragma`: The pragma header. + - `cache_control`: The cache control header. + - `user_agent`: The user agent string of the client. + - `sec_websocket_version`: The websocket version. + - `sec_websocket_key`: The websocket key. + - `sec_websocket_extensions`: The websocket extensions. + - `accept_encoding`: The accepted encodings. + - `accept_language`: The accepted languages. + - `raw_headers`: A mapping of all HTTP headers as a frozen dictionary. This provides access to any header that was sent with the request, not just the common ones listed above. + +### Example Values on this Page + +```python eval +rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Name"), + rx.table.column_header_cell("Value"), + ), + ), + rx.table.body( + *[ + rx.table.row( + rx.table.cell(item["name"], style=cell_style), + rx.table.cell(rx.code(item["value"], style=get_code_style("violet"))), + ) + for item in router_data + ] + ), + variant="surface", + margin_y="1em", + ) +``` + +### Accessing Raw Headers + +The `raw_headers` attribute provides access to all HTTP headers as a frozen dictionary. This is useful when you need to access headers that are not explicitly defined in the `HeaderData` class: + +```python box +# Access a specific header +custom_header_value = self.router.headers.raw_headers.get("x-custom-header", "") + +# Example of accessing common headers +user_agent = self.router.headers.raw_headers.get("user-agent", "") +content_type = self.router.headers.raw_headers.get("content-type", "") +authorization = self.router.headers.raw_headers.get("authorization", "") + +# You can also check if a header exists +has_custom_header = "x-custom-header" in self.router.headers.raw_headers +``` + +This is particularly useful for accessing custom headers or when working with specific HTTP headers that are not part of the standard set exposed as direct attributes. diff --git a/docs/vars/base_vars.md b/docs/vars/base_vars.md new file mode 100644 index 00000000000..b75bb8e161c --- /dev/null +++ b/docs/vars/base_vars.md @@ -0,0 +1,226 @@ +```python exec +import random +import time + +import reflex as rx + +from pcweb.pages.docs import vars +``` + +# Base Vars + +Vars are any fields in your app that may change over time. A Var is directly +rendered into the frontend of the app. + +Base vars are defined as fields in your State class. + +They can have a preset default value. If you don't provide a default value, you +must provide a type annotation. + +```md alert warning +# State Vars should provide type annotations. + +Reflex relies on type annotations to determine the type of state vars during the compilation process. +``` + +```python demo exec +class TickerState(rx.State): + ticker: str ="AAPL" + price: str = "$150" + + +def ticker_example(): + return rx.center( + rx.vstack( + rx.heading(TickerState.ticker, size="3"), + rx.text(f"Current Price: {TickerState.price}", font_size="md"), + rx.text("Change: 4%", color="green"), + ), + + ) +``` + +In this example `ticker` and `price` are base vars in the app, which can be modified at runtime. + +```md alert warning +# Vars must be JSON serializable. + +Vars are used to communicate between the frontend and backend. They must be primitive Python types, Plotly figures, Pandas dataframes, or [a custom defined type]({vars.custom_vars.path}). +``` + +## Accessing state variables on different pages + +State is just a python class and so can be defined on one page and then imported and used on another. Below we define `TickerState` class on the page `state.py` and then import it and use it on the page `index.py`. + +```python +# state.py + +class TickerState(rx.State): + ticker: str = "AAPL" + price: str = "$150" +``` + +```python +# index.py +from .state import TickerState + +def ticker_example(): + return rx.center( + rx.vstack( + rx.heading(TickerState.ticker, size="3"), + rx.text(f"Current Price: \{TickerState.price}", font_size="md"), + rx.text("Change: 4%", color="green"), + ), + + ) +``` + +## Backend-only Vars + +Any Var in a state class that starts with an underscore (`_`) is considered backend +only and will **not be synchronized with the frontend**. Data associated with a +specific session that is _not directly rendered on the frontend should be stored +in a backend-only var_ to reduce network traffic and improve performance. + +They have the advantage that they don't need to be JSON serializable, however +they must still be pickle-able to be used with redis in prod mode. They are +not directly renderable on the frontend, and **may be used to store sensitive +values that should not be sent to the client**. + +```md alert warning +# Protect auth data and sensitive state in backend-only vars. + +Regular vars and computed vars should **only** be used for rendering the state +of your app in the frontend. Having any type of permissions or authenticated state based on +a regular var presents a security risk as you may assume these have shared control +with the frontend (client) due to default setter methods. + +For improved security, `state_auto_setters=False` may be set in `rxconfig.py` +to prevent the automatic generation of setters for regular vars, however, the +client will still be able to locally modify the contents of frontend vars as +they are presented in the UI. +``` + +For example, a backend-only var is used to store a large data structure which is +then paged to the frontend using cached vars. + +```python demo exec +import numpy as np + + +class BackendVarState(rx.State): + _backend: np.ndarray = np.array([random.randint(0, 100) for _ in range(100)]) + offset: int = 0 + limit: int = 10 + + @rx.var(cache=True) + def page(self) -> list[int]: + return [ + int(x) # explicit cast to int + for x in self._backend[self.offset : self.offset + self.limit] + ] + + @rx.var(cache=True) + def page_number(self) -> int: + return (self.offset // self.limit) + 1 + (1 if self.offset % self.limit else 0) + + @rx.var(cache=True) + def total_pages(self) -> int: + return len(self._backend) // self.limit + (1 if len(self._backend) % self.limit else 0) + + @rx.event + def prev_page(self): + self.offset = max(self.offset - self.limit, 0) + + @rx.event + def next_page(self): + if self.offset + self.limit < len(self._backend): + self.offset += self.limit + + @rx.event + def generate_more(self): + self._backend = np.append(self._backend, [random.randint(0, 100) for _ in range(random.randint(0, 100))]) + + @rx.event + def set_limit(self, value: str): + self.limit = int(value) + +def backend_var_example(): + return rx.vstack( + rx.hstack( + rx.button( + "Prev", + on_click=BackendVarState.prev_page, + ), + rx.text(f"Page {BackendVarState.page_number} / {BackendVarState.total_pages}"), + rx.button( + "Next", + on_click=BackendVarState.next_page, + ), + rx.text("Page Size"), + rx.input( + width="5em", + value=BackendVarState.limit, + on_change=BackendVarState.set_limit, + ), + rx.button("Generate More", on_click=BackendVarState.generate_more), + ), + rx.list( + rx.foreach( + BackendVarState.page, + lambda x, ix: rx.text(f"_backend[{ix + BackendVarState.offset}] = {x}"), + ), + ), + ) +``` + +## Using rx.field / rx.Field to improve type hinting for vars + +When defining state variables you can use `rx.Field[T]` to annotate the variable's type. Then, you can initialize the variable using `rx.field(default_value)`, where `default_value` is an instance of type `T`. + +This approach makes the variable's type explicit, aiding static analysis tools in type checking. In addition, it shows you what methods are allowed to modify the variable in your frontend code, as they are listed in the type hint. + +Below are two examples: + +```python +import reflex as rx + +app = rx.App() + + +class State(rx.State): + x: rx.Field[bool] = rx.field(False) + + def flip(self): + self.x = not self.x + + +@app.add_page +def index(): + return rx.vstack( + rx.button("Click me", on_click=State.flip), + rx.text(State.x), + rx.text(~State.x), + ) +``` + +Here `State.x`, as it is typed correctly as a `boolean` var, gets better code completion, i.e. here we get options such as `to_string()` or `equals()`. + +```python +import reflex as rx + +app = rx.App() + + +class State(rx.State): + x: rx.Field[dict[str, list[int]]] = rx.field(default_factory=dict) + + +@app.add_page +def index(): + return rx.vstack( + rx.text(State.x.values()[0][0]), + ) +``` + +Here `State.x`, as it is typed correctly as a `dict` of `str` to `list` of `int` var, gets better code completion, i.e. here we get options such as `contains()`, `keys()`, `values()`, `items()` or `merge()`. diff --git a/docs/vars/computed_vars.md b/docs/vars/computed_vars.md new file mode 100644 index 00000000000..ce1bef92d13 --- /dev/null +++ b/docs/vars/computed_vars.md @@ -0,0 +1,190 @@ +```python exec +import random +import time +import asyncio + +import reflex as rx +``` + +# Computed Vars + +Computed vars have values derived from other properties on the backend. They are +defined as methods in your State class with the `@rx.var` decorator. + +Try typing in the input box and clicking out. + +```python demo exec id=upper +class UppercaseState(rx.State): + text: str = "hello" + + def set_text(self, value: str): + self.text = value + + @rx.var + def upper_text(self) -> str: + # This will be recomputed whenever `text` changes. + return self.text.upper() + + +def uppercase_example(): + return rx.vstack( + rx.heading(UppercaseState.upper_text), + rx.input(on_blur=UppercaseState.set_text, placeholder="Type here..."), + ) +``` + +Here, `upper_text` is a computed var that always holds the upper case version of `text`. + +We recommend always using type annotations for computed vars. + +## Cached Vars + +By default, all computed vars are cached (`cache=True`). A cached var is only +recomputed when the other state vars it depends on change. This is useful for +expensive computations, but in some cases it may not update when you expect it to. + +To create a computed var that recomputes on every state update regardless of +dependencies, use `@rx.var(cache=False)`. + +Previous versions of Reflex had a `@rx.cached_var` decorator, which is now replaced +by the `cache` argument of `@rx.var` (which defaults to `True`). + +```python demo exec +class CachedVarState(rx.State): + counter_a: int = 0 + counter_b: int = 0 + + @rx.var(cache=False) + def last_touch_time(self) -> str: + # This is updated anytime the state is updated. + return time.strftime("%H:%M:%S") + + @rx.event + def increment_a(self): + self.counter_a += 1 + + @rx.var(cache=True) + def last_counter_a_update(self) -> str: + # This is updated only when `counter_a` changes. + return f"{self.counter_a} at {time.strftime('%H:%M:%S')}" + + @rx.event + def increment_b(self): + self.counter_b += 1 + + @rx.var(cache=True) + def last_counter_b_update(self) -> str: + # This is updated only when `counter_b` changes. + return f"{self.counter_b} at {time.strftime('%H:%M:%S')}" + + +def cached_var_example(): + return rx.vstack( + rx.text(f"State touched at: {CachedVarState.last_touch_time}"), + rx.text(f"Counter A: {CachedVarState.last_counter_a_update}"), + rx.text(f"Counter B: {CachedVarState.last_counter_b_update}"), + rx.hstack( + rx.button("Increment A", on_click=CachedVarState.increment_a), + rx.button("Increment B", on_click=CachedVarState.increment_b), + ), + ) +``` + +In this example `last_touch_time` uses `cache=False` to ensure it updates any +time the state is modified. `last_counter_a_update` is a cached computed var (using +the default `cache=True`) that only depends on `counter_a`, so it only gets recomputed +when `counter_a` changes. Similarly `last_counter_b_update` only depends on `counter_b`, +and thus is updated only when `counter_b` changes. + +## Async Computed Vars + +Async computed vars allow you to use asynchronous operations in your computed vars. +They are defined as async methods in your State class with the same `@rx.var` decorator. +Async computed vars are useful for operations that require asynchronous processing, such as: + +- Fetching data from external APIs +- Database operations +- File I/O operations +- Any other operations that benefit from async/await + +```python demo exec +class AsyncVarState(rx.State): + count: int = 0 + + @rx.var + async def delayed_count(self) -> int: + # Simulate an async operation like an API call + await asyncio.sleep(0.5) + return self.count * 2 + + @rx.event + def increment(self): + self.count += 1 + + +def async_var_example(): + return rx.vstack( + rx.heading("Async Computed Var Example"), + rx.text(f"Count: {AsyncVarState.count}"), + rx.text(f"Delayed count (x2): {AsyncVarState.delayed_count}"), + rx.button("Increment", on_click=AsyncVarState.increment), + spacing="4", + ) +``` + +In this example, `delayed_count` is an async computed var that returns the count multiplied by 2 after a simulated delay. +When the count changes, the async computed var is automatically recomputed. + +### Caching Async Computed Vars + +Just like regular computed vars, async computed vars can also be cached. This is especially +useful for expensive async operations like API calls or database queries. + +```python demo exec +class AsyncCachedVarState(rx.State): + user_id: int = 1 + refresh_trigger: int = 0 + + @rx.var(cache=True) + async def user_data(self) -> dict: + # In a real app, this would be an API call + await asyncio.sleep(1) # Simulate network delay + + # Simulate different user data based on user_id + users = { + 1: {"name": "Alice", "email": "alice@example.com"}, + 2: {"name": "Bob", "email": "bob@example.com"}, + 3: {"name": "Charlie", "email": "charlie@example.com"}, + } + + return users.get(self.user_id, {"name": "Unknown", "email": "unknown"}) + + @rx.event + def change_user(self): + # Cycle through users 1-3 + self.user_id = (self.user_id % 3) + 1 + + @rx.event + def force_refresh(self): + # This will not affect user_data dependencies, but will trigger a state update + self.refresh_trigger += 1 + + +def async_cached_var_example(): + return rx.vstack( + rx.heading("Cached Async Computed Var Example"), + rx.text(f"User ID: {AsyncCachedVarState.user_id}"), + rx.text(f"User Name: {AsyncCachedVarState.user_data['name']}"), + rx.text(f"User Email: {AsyncCachedVarState.user_data['email']}"), + rx.hstack( + rx.button("Change User", on_click=AsyncCachedVarState.change_user), + rx.button("Force Refresh (No Effect)", on_click=AsyncCachedVarState.force_refresh), + ), + rx.text("Note: The cached async var only updates when user_id changes, not when refresh_trigger changes."), + spacing="4", + ) +``` + +In this example, `user_data` is a cached async computed var that simulates fetching user data. +It is only recomputed when `user_id` changes, not when other state variables like `refresh_trigger` change. +This demonstrates how caching works with async computed vars to optimize performance for expensive operations. diff --git a/docs/vars/custom_vars.md b/docs/vars/custom_vars.md new file mode 100644 index 00000000000..4de84b059b1 --- /dev/null +++ b/docs/vars/custom_vars.md @@ -0,0 +1,82 @@ +```python exec +import reflex as rx +import dataclasses +from typing import TypedDict + +from pcweb.pages.docs import vars +``` + +# Custom Vars + +As mentioned in the [vars page]({vars.base_vars.path}), Reflex vars must be JSON serializable. + +This means we can support any Python primitive types, as well as lists, dicts, and tuples. However, you can also create more complex var types using dataclasses (recommended), TypedDict, or Pydantic models. + +## Defining a Type + +In this example, we will create a custom var type for storing translations using a dataclass. + +Once defined, we can use it as a state var, and reference it from within a component. + +```python demo exec +import googletrans +import dataclasses +from typing import TypedDict + +@dataclasses.dataclass +class Translation: + original_text: str + translated_text: str + +class TranslationState(rx.State): + input_text: str = "Hola Mundo" + current_translation: Translation = Translation(original_text="", translated_text="") + + # Explicitly define the setter method + def set_input_text(self, value: str): + self.input_text = value + + @rx.event + def translate(self): + self.current_translation.original_text = self.input_text + self.current_translation.translated_text = googletrans.Translator().translate(self.input_text, dest="en").text + +def translation_example(): + return rx.vstack( + rx.input( + on_blur=TranslationState.set_input_text, + default_value=TranslationState.input_text, + placeholder="Text to translate...", + ), + rx.button("Translate", on_click=TranslationState.translate), + rx.text(TranslationState.current_translation.translated_text), + ) +``` + +## Alternative Approaches + +### Using TypedDict + +You can also use TypedDict for defining custom var types: + +```python +from typing import TypedDict + +class Translation(TypedDict): + original_text: str + translated_text: str +``` + +### Using Pydantic Models + +Pydantic models are another option for complex data structures: + +```python +from pydantic import BaseModel + +class Translation(BaseModel): + original_text: str + translated_text: str +``` + +For complex data structures, dataclasses are recommended as they provide a clean, type-safe way to define custom var types with good IDE support. diff --git a/docs/vars/var-operations.md b/docs/vars/var-operations.md new file mode 100644 index 00000000000..7a56a73e798 --- /dev/null +++ b/docs/vars/var-operations.md @@ -0,0 +1,555 @@ +```python exec +import random +import time + +import numpy as np + +import reflex as rx + +from pcweb.templates.docpage import docpage +``` + +# Var Operations + +Var operations transform the placeholder representation of the value on the +frontend and provide a way to perform basic operations on the Var without having +to define a computed var. + +Within your frontend components, you cannot use arbitrary Python functions on +the state vars. For example, the following code will **not work.** + +```python +class State(rx.State): + number: int + +def index(): + # This will be compiled before runtime, before `State.number` has a known value. + # Since `float` is not a valid var operation, this will throw an error. + rx.text(float(State.number)) +``` + +This is because we compile the frontend to Javascript, but the value of `State.number` +is only known at runtime. + +In this example below we use a var operation to concatenate a `string` with a `var`, meaning we do not have to do in within state as a computed var. + +```python demo exec +coins = ["BTC", "ETH", "LTC", "DOGE"] + +class VarSelectState(rx.State): + selected: str = "DOGE" + + def set_selected(self, value: str): + self.selected = value + +def var_operations_example(): + return rx.vstack( + # Using a var operation to concatenate a string with a var. + rx.heading("I just bought a bunch of " + VarSelectState.selected), + # Using an f-string to interpolate a var. + rx.text(f"{VarSelectState.selected} is going to the moon!"), + rx.select( + coins, + value=VarSelectState.selected, + on_change=VarSelectState.set_selected, + ) + ) +``` + +```md alert success +# Vars support many common operations. + +They can be used for arithmetic, string concatenation, inequalities, indexing, and more. See the [full list of supported operations](/docs/api-reference/var/). +``` + +## Supported Operations + +Var operations allow us to change vars on the front-end without having to create more computed vars on the back-end in the state. + +Some simple examples are the `==` var operator, which is used to check if two vars are equal and the `to_string()` var operator, which is used to convert a var to a string. + +```python demo exec + +fruits = ["Apple", "Banana", "Orange", "Mango"] + +class EqualsState(rx.State): + selected: str = "Apple" + favorite: str = "Banana" + + def set_selected(self, value: str): + self.selected = value + + +def var_equals_example(): + return rx.vstack( + rx.text(EqualsState.favorite.to_string() + " is my favorite fruit!"), + rx.select( + fruits, + value=EqualsState.selected, + on_change=EqualsState.set_selected, + ), + rx.cond( + EqualsState.selected == EqualsState.favorite, + rx.text("The selected fruit is equal to the favorite fruit!"), + rx.text("The selected fruit is not equal to the favorite fruit."), + ), + ) + +``` + +### Negate, Absolute and Length + +The `-` operator is used to get the negative version of the var. The `abs()` operator is used to get the absolute value of the var. The `.length()` operator is used to get the length of a list var. + +```python demo exec +import random + +class OperState(rx.State): + number: int + numbers_seen: list = [] + + @rx.event + def update(self): + self.number = random.randint(-100, 100) + self.numbers_seen.append(self.number) + +def var_operation_example(): + return rx.vstack( + rx.heading(f"The number: {OperState.number}", size="3"), + rx.hstack( + rx.text("Negated:", rx.badge(-OperState.number, variant="soft", color_scheme="green")), + rx.text(f"Absolute:", rx.badge(abs(OperState.number), variant="soft", color_scheme="blue")), + rx.text(f"Numbers seen:", rx.badge(OperState.numbers_seen.length(), variant="soft", color_scheme="red")), + ), + rx.button("Update", on_click=OperState.update), + ) +``` + +### Comparisons and Mathematical Operators + +All of the comparison operators are used as expected in python. These include `==`, `!=`, `>`, `>=`, `<`, `<=`. + +There are operators to add two vars `+`, subtract two vars `-`, multiply two vars `*` and raise a var to a power `pow()`. + +```python demo exec +import random + +class CompState(rx.State): + number_1: int + number_2: int + + @rx.event + def update(self): + self.number_1 = random.randint(-10, 10) + self.number_2 = random.randint(-10, 10) + +def var_comparison_example(): + + return rx.vstack( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Integer 1"), + rx.table.column_header_cell("Integer 2"), + rx.table.column_header_cell("Operation"), + rx.table.column_header_cell("Outcome"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 == Int 2"), + rx.table.cell((CompState.number_1 == CompState.number_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 != Int 2"), + rx.table.cell((CompState.number_1 != CompState.number_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 > Int 2"), + rx.table.cell((CompState.number_1 > CompState.number_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 >= Int 2"), + rx.table.cell((CompState.number_1 >= CompState.number_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2, ), + rx.table.cell("Int 1 < Int 2 "), + rx.table.cell((CompState.number_1 < CompState.number_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 <= Int 2"), + rx.table.cell((CompState.number_1 <= CompState.number_2).to_string()), + ), + + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 + Int 2"), + rx.table.cell(f"{(CompState.number_1 + CompState.number_2)}"), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 - Int 2"), + rx.table.cell(f"{CompState.number_1 - CompState.number_2}"), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("Int 1 * Int 2"), + rx.table.cell(f"{CompState.number_1 * CompState.number_2}"), + ), + rx.table.row( + rx.table.row_header_cell(CompState.number_1), + rx.table.cell(CompState.number_2), + rx.table.cell("pow(Int 1, Int2)"), + rx.table.cell(f"{pow(CompState.number_1, CompState.number_2)}"), + ), + ), + width="100%", + ), + rx.button("Update", on_click=CompState.update), + ) +``` + +### True Division, Floor Division and Remainder + +The operator `/` represents true division. The operator `//` represents floor division. The operator `%` represents the remainder of the division. + +```python demo exec +import random + +class DivState(rx.State): + number_1: float = 3.5 + number_2: float = 1.4 + + @rx.event + def update(self): + self.number_1 = round(random.uniform(5.1, 9.9), 2) + self.number_2 = round(random.uniform(0.1, 4.9), 2) + +def var_div_example(): + return rx.vstack( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Integer 1"), + rx.table.column_header_cell("Integer 2"), + rx.table.column_header_cell("Operation"), + rx.table.column_header_cell("Outcome"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell(DivState.number_1), + rx.table.cell(DivState.number_2), + rx.table.cell("Int 1 / Int 2"), + rx.table.cell(f"{DivState.number_1 / DivState.number_2}"), + ), + rx.table.row( + rx.table.row_header_cell(DivState.number_1), + rx.table.cell(DivState.number_2), + rx.table.cell("Int 1 // Int 2"), + rx.table.cell(f"{DivState.number_1 // DivState.number_2}"), + ), + rx.table.row( + rx.table.row_header_cell(DivState.number_1), + rx.table.cell(DivState.number_2), + rx.table.cell("Int 1 % Int 2"), + rx.table.cell(f"{DivState.number_1 % DivState.number_2}"), + ), + ), + width="100%", + ), + rx.button("Update", on_click=DivState.update), + ) +``` + +### And, Or and Not + +In Reflex the `&` operator represents the logical AND when used in the front end. This means that it returns true only when both conditions are true simultaneously. +The `|` operator represents the logical OR when used in the front end. This means that it returns true when either one or both conditions are true. +The `~` operator is used to invert a var. It is used on a var of type `bool` and is equivalent to the `not` operator. + +```python demo exec +import random + +class LogicState(rx.State): + var_1: bool = True + var_2: bool = True + + @rx.event + def update(self): + self.var_1 = random.choice([True, False]) + self.var_2 = random.choice([True, False]) + +def var_logical_example(): + return rx.vstack( + rx.table.root( + rx.table.header( + rx.table.row( + rx.table.column_header_cell("Var 1"), + rx.table.column_header_cell("Var 2"), + rx.table.column_header_cell("Operation"), + rx.table.column_header_cell("Outcome"), + ), + ), + rx.table.body( + rx.table.row( + rx.table.row_header_cell(LogicState.var_1.to_string()), + rx.table.cell(LogicState.var_2.to_string()), + rx.table.cell("Logical AND (&)"), + rx.table.cell((LogicState.var_1 & LogicState.var_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(LogicState.var_1.to_string()), + rx.table.cell(LogicState.var_2.to_string()), + rx.table.cell("Logical OR (|)"), + rx.table.cell((LogicState.var_1 | LogicState.var_2).to_string()), + ), + rx.table.row( + rx.table.row_header_cell(LogicState.var_1.to_string()), + rx.table.cell(LogicState.var_2.to_string()), + rx.table.cell("The invert of Var 1 (~)"), + rx.table.cell((~LogicState.var_1).to_string()), + ), + + ), + width="100%", + ), + rx.button("Update", on_click=LogicState.update), + ) +``` + +### Contains, Reverse and Join + +The 'in' operator is not supported for Var types, we must use the `Var.contains()` instead. When we use `contains`, the var must be of type: `dict`, `list`, `tuple` or `str`. +`contains` checks if a var contains the object that we pass to it as an argument. + +We use the `reverse` operation to reverse a list var. The var must be of type `list`. + +Finally we use the `join` operation to join a list var into a string. + +```python demo exec +class ListsState(rx.State): + list_1: list = [1, 2, 3, 4, 6] + list_2: list = [7, 8, 9, 10] + list_3: list = ["p","y","t","h","o","n"] + +def var_list_example(): + return rx.hstack( + rx.vstack( + rx.heading(f"List 1: {ListsState.list_1}", size="3"), + rx.text(f"List 1 Contains 3: {ListsState.list_1.contains(3)}"), + ), + rx.vstack( + rx.heading(f"List 2: {ListsState.list_2}", size="3"), + rx.text(f"Reverse List 2: {ListsState.list_2.reverse()}"), + ), + rx.vstack( + rx.heading(f"List 3: {ListsState.list_3}", size="3"), + rx.text(f"List 3 Joins: {ListsState.list_3.join()}"), + ), + ) +``` + +### Lower, Upper, Split + +The `lower` operator converts a string var to lowercase. The `upper` operator converts a string var to uppercase. The `split` operator splits a string var into a list. + +```python demo exec +class StringState(rx.State): + string_1: str = "PYTHON is FUN" + string_2: str = "react is hard" + + +def var_string_example(): + return rx.hstack( + rx.vstack( + rx.heading(f"List 1: {StringState.string_1}", size="3"), + rx.text(f"List 1 Lower Case: {StringState.string_1.lower()}"), + ), + rx.vstack( + rx.heading(f"List 2: {StringState.string_2}", size="3"), + rx.text(f"List 2 Upper Case: {StringState.string_2.upper()}"), + rx.text(f"Split String 2: {StringState.string_2.split()}"), + ), + ) +``` + +## Get Item (Indexing) + +Indexing is only supported for strings, lists, tuples, dicts, and dataframes. To index into a state var strict type annotations are required. + +```python +class GetItemState1(rx.State): + list_1: list = [50, 10, 20] + +def get_item_error_1(): + return rx.progress(value=GetItemState1.list_1[0]) +``` + +In the code above you would expect to index into the first index of the list_1 state var. In fact the code above throws the error: `Invalid var passed for prop value, expected type , got value of type typing.Any.` This is because the type of the items inside the list have not been clearly defined in the state. To fix this you change the list_1 definition to `list_1: list[int] = [50, 10, 20]` + +```python demo exec +class GetItemState1(rx.State): + list_1: list[int] = [50, 10, 20] + +def get_item_error_1(): + return rx.progress(value=GetItemState1.list_1[0]) +``` + +### Using with Foreach + +Errors frequently occur when using indexing and `foreach`. + +```python +class ProjectsState(rx.State): + projects: List[dict] = [ + { + "technologies": ["Next.js", "Prisma", "Tailwind", "Google Cloud", "Docker", "MySQL"] + }, + { + "technologies": ["Python", "Flask", "Google Cloud", "Docker"] + } + ] + +def get_badge(technology: str) -> rx.Component: + return rx.badge(technology, variant="soft", color_scheme="green") + +def project_item(project: dict): + return rx.box( + rx.hstack( + rx.foreach(project["technologies"], get_badge) + ), + ) + +def failing_projects_example() -> rx.Component: + return rx.box(rx.foreach(ProjectsState.projects, project_item)) +``` + +The code above throws the error `TypeError: Could not foreach over var of type Any. (If you are trying to foreach over a state var, add a type annotation to the var.)` + +We must change `projects: list[dict]` => `projects: list[dict[str, list]]` because while projects is annotated, the item in project["technologies"] is not. + +```python demo exec +class ProjectsState(rx.State): + projects: list[dict[str, list]] = [ + { + "technologies": ["Next.js", "Prisma", "Tailwind", "Google Cloud", "Docker", "MySQL"] + }, + { + "technologies": ["Python", "Flask", "Google Cloud", "Docker"] + } + ] + + +def projects_example() -> rx.Component: + def get_badge(technology: str) -> rx.Component: + return rx.badge(technology, variant="soft", color_scheme="green") + + def project_item(project: dict) -> rx.Component: + + return rx.box( + rx.hstack( + rx.foreach(project["technologies"], get_badge) + ), + ) + return rx.box(rx.foreach(ProjectsState.projects, project_item)) +``` + +The previous example had only a single type for each of the dictionaries `keys` and `values`. For complex multi-type data, you need to use a dataclass, as shown below. + +```python demo exec +import dataclasses + +@dataclasses.dataclass +class ActressType: + actress_name: str + age: int + pages: list[dict[str, str]] + +class MultiDataTypeState(rx.State): + """The app state.""" + actresses: list[ActressType] = [ + ActressType( + actress_name="Ariana Grande", + age=30, + pages=[ + {"url": "arianagrande.com"}, {"url": "https://es.wikipedia.org/wiki/Ariana_Grande"} + ] + ), + ActressType( + actress_name="Gal Gadot", + age=38, + pages=[ + {"url": "http://www.galgadot.com/"}, {"url": "https://es.wikipedia.org/wiki/Gal_Gadot"} + ] + ) + ] + +def actresses_example() -> rx.Component: + def showpage(page: dict[str, str]): + return rx.vstack( + rx.text(page["url"]), + ) + + def showlist(item: ActressType): + return rx.vstack( + rx.hstack( + rx.text(item.actress_name), + rx.text(item.age), + ), + rx.foreach(item.pages, showpage), + ) + return rx.box(rx.foreach(MultiDataTypeState.actresses, showlist)) + +``` + +Setting the type of `actresses` to be `actresses: list[dict[str,str]]` would fail as it cannot be understood that the `value` for the `pages key` is actually a `list`. + +## Combine Multiple Var Operations + +You can also combine multiple var operations together, as seen in the next example. + +```python demo exec +import random + +class VarNumberState(rx.State): + number: int + + @rx.event + def update(self): + self.number = random.randint(0, 100) + +def var_number_example(): + return rx.vstack( + rx.heading(f"The number is {VarNumberState.number}", size="5"), + # Var operations can be composed for more complex expressions. + rx.cond( + VarNumberState.number % 2 == 0, + rx.text("Even", color="green"), + rx.text("Odd", color="red"), + ), + rx.button("Update", on_click=VarNumberState.update), + ) +``` + +We could have made a computed var that returns the parity of `number`, but +it can be simpler just to use a var operation instead. + +Var operations may be generally chained to make compound expressions, however +some complex transformations not supported by var operations must use computed vars +to calculate the value on the backend. diff --git a/docs/vi/README.md b/docs/vi/README.md deleted file mode 100644 index b8bc6637d68..00000000000 --- a/docs/vi/README.md +++ /dev/null @@ -1,255 +0,0 @@ -
-Reflex Logo -
- -### **✨ Ứng dụng web hiệu suất cao, tùy chỉnh bằng Python thuần. Deploy trong vài giây. ✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) - -
- ---- - -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex là một thư viện để xây dựng ứng dụng web toàn bộ bằng Python thuần. - -Các tính năng chính: - -- **Python thuần tuý** - Viết toàn bộ ứng dụng cả backend và frontend hoàn toàn bằng Python, không cần học JavaScript. -- **Full Flexibility** - Reflex dễ dàng để bắt đầu, nhưng cũng có thể mở rộng lên các ứng dụng phức tạp. -- **Deploy Instantly** - Sau khi xây dựng ứng dụng, bạn có thể triển khai bằng [một dòng lệnh](https://reflex.dev/docs/hosting/deploy-quick-start/) hoặc triển khai trên server của riêng bạn. - -Đọc [bài viết về kiến trúc hệ thống](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) để hiểu rõ các hoạt động của Reflex. - -## ⚙️ Cài đặt - -Mở cửa sổ lệnh và chạy (Yêu cầu Python phiên bản 3.10+): - -```bash -pip install reflex -``` - -## 🥳 Tạo ứng dụng đầu tiên - -Cài đặt `reflex` cũng như cài đặt công cụ dòng lệnh `reflex`. - -Kiểm tra việc cài đặt đã thành công hay chưa bằng cách tạo mới một ứng dụng. (Thay `my_app_name` bằng tên ứng dụng của bạn): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -Lệnh này tạo ra một ứng dụng mẫu trong một thư mục mới. - -Bạn có thể chạy ứng dụng ở chế độ phát triển. - -```bash -reflex run -``` - -Bạn có thể xem ứng dụng của bạn ở địa chỉ http://localhost:3000. - -Bạn có thể thay đổi mã nguồn ở `my_app_name/my_app_name.py`. Reflex nhanh chóng làm mới và bạn có thể thấy thay đổi trên ứng dụng của bạn ngay lập tức khi bạn lưu file. - -## 🫧 Ứng dụng ví dụ - -Bắt đầu với ví dụ: tạo một ứng dụng tạo ảnh bằng [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). Để cho đơn giản, chúng ta sẽ sử dụng [OpenAI API](https://platform.openai.com/docs/api-reference/authentication), nhưng bạn có thể sử dụng model của chính bạn được triển khai trên local. - -  - -
-A frontend wrapper for DALL·E, shown in the process of generating an image. -
- -  - -Đây là toàn bộ đoạn mã để xây dựng ứng dụng trên. Nó được viết hoàn toàn trong một file Python! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """The app state.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True - - -def index(): - return rx.center( - rx.vstack( - rx.heading("DALL-E", font_size="1.5em"), - rx.input( - placeholder="Enter a prompt..", - on_blur=State.set_prompt, - width="25em", - ), - rx.button( - "Generate Image", - on_click=State.get_image, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Add state and page to the app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## Hãy phân tích chi tiết. - -
-Explaining the differences between backend and frontend parts of the DALL-E app. -
- -### **Reflex UI** - -Bắt đầu với giao diện chính. - -```python -def index(): - return rx.center( - ... - ) -``` - -Hàm `index` định nghĩa phần giao diện chính của ứng dụng. - -Chúng tôi sử dụng các component (thành phần) khác nhau như `center`, `vstack`, `input` và `button` để xây dựng giao diện phía trước. -Các component có thể được lồng vào nhau để tạo ra các bố cục phức tạp. Và bạn cũng có thể sử dụng từ khoá `args` để tận dụng đầy đủ sức mạnh của CSS. - -Reflex có đến hơn [60 component được xây dựng sẵn](https://reflex.dev/docs/library) để giúp bạn bắt đầu. Chúng ta có thể tạo ra một component mới khá dễ dàng, thao khảo: [xây dựng component của riêng bạn](https://reflex.dev/docs/wrapping-react/overview/). - -### **State** - -Reflex biểu diễn giao diện bằng các hàm của state (trạng thái). - -```python -class State(rx.State): - """The app state.""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -Một state định nghĩa các biến (được gọi là vars) có thể thay đổi trong một ứng dụng và cho phép các hàm có thể thay đổi chúng. - -Tại đây state được cấu thành từ một `prompt` và `image_url`. -Có cũng những biến boolean `processing` và `complete` -để chỉ ra khi nào tắt nút (trong quá trình tạo hình ảnh) -và khi nào hiển thị hình ảnh kết quả. - -### **Event Handlers** - -```python -def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True -``` - -Với các state, chúng ta định nghĩa các hàm có thể thay đổi state vars được gọi là event handlers. Event handler là cách chúng ta có thể thay đổi state trong Reflex. Chúng có thể là phản hồi khi người dùng thao tác, chằng hạn khi nhấn vào nút hoặc khi đang nhập trong text box. Các hành động này được gọi là event. - -Ứng dụng DALL·E. của chúng ta có một event handler, `get_image` để lấy hình ảnh từ OpenAI API. Sử dụng từ khoá `yield` in ở giữa event handler để cập nhật giao diện. Hoặc giao diện có thể cập nhật ở cuối event handler. - -### **Routing** - -Cuối cùng, chúng ta định nghĩa một ứng dụng. - -```python -app = rx.App() -``` - -Chúng ta thêm một trang ở đầu ứng dụng bằng index component. Chúng ta cũng thêm tiêu đề của ứng dụng để hiển thị lên trình duyệt. - -```python -app.add_page(index, title="DALL-E") -``` - -Bạn có thể tạo một ứng dụng nhiều trang bằng cách thêm trang. - -## 📑 Tài liệu - -
- -📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Component Library](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Status - -Reflex phát hành vào tháng 12/2022 với tên là Pynecone. - -Từ năm 2025, [Reflex Cloud](https://cloud.reflex.dev) đã ra mắt để cung cấp trải nghiệm lưu trữ tốt nhất cho các ứng dụng Reflex. Chúng tôi sẽ tiếp tục phát triển và triển khai thêm nhiều tính năng mới. - -Reflex ra phiên bản mới với các tính năng mới hàng tuần! Hãy :star: star và :eyes: watch repo này để thấy các cập nhật mới nhất. - -## Contributing - -Chúng tôi chào đón mọi đóng góp dù lớn hay nhỏ. Dưới đây là các cách để bắt đầu với cộng đồng Reflex. - -- **Discord**: [Discord](https://discord.gg/T5WSbC2YtQ) của chúng tôi là nơi tốt nhất để nhờ sự giúp đỡ và thảo luận các bạn có thể đóng góp. -- **GitHub Discussions**: Là cách tốt nhất để thảo luận về các tính năng mà bạn có thể đóng góp hoặc những điều bạn chưa rõ. -- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues) là nơi tốt nhất để thông báo. Ngoài ra bạn có thể sửa chữa các vấn đề bằng cách tạo PR. - -Chúng tôi luôn sẵn sàng tìm kiếm các contributor, bất kể kinh nghiệm. Để tham gia đóng góp, xin mời xem -[CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) - -## Xin cảm ơn các Contributors: - - - - - -## License - -Reflex là mã nguồn mở và sử dụng giấy phép [Apache License 2.0](/LICENSE). diff --git a/docs/wrapping-react/custom-code-and-hooks.md b/docs/wrapping-react/custom-code-and-hooks.md new file mode 100644 index 00000000000..91b39412afe --- /dev/null +++ b/docs/wrapping-react/custom-code-and-hooks.md @@ -0,0 +1,109 @@ +When wrapping a React component, you may need to define custom code or hooks that are specific to the component. This is done by defining the `add_custom_code`or `add_hooks` methods in your component class. + +## Custom Code + +Custom code is any JS code that need to be included in your page, but not necessarily in the component itself. This can include things like CSS styles, JS libraries, or any other code that needs to be included in the page. + +```python +class CustomCodeComponent(MyBaseComponent): + """MyComponent.""" + + def add_custom_code(self) -> list[str]: + """Add custom code to the component.""" + code1 = """const customVariable = "Custom code1";""" + code2 = """console.log(customVariable);""" + + return [code1, code2] +``` + +The above example will render the following JS code in the page: + +```javascript +/* import here */ + +const customVariable = "Custom code1"; +console.log(customVariable); + +/* rest of the page code */ +``` + +## Custom Hooks + +Custom hooks are any hooks that need to be included in your component. This can include things like `useEffect`, `useState`, or any other hooks from the library you are wrapping. + +- Simple hooks can be added as strings. +- More complex hooks that need to have special import or be written in a specific order can be added as `rx.Var` with a `VarData` object to specify the position of the hook. + - The `imports` attribute of the `VarData` object can be used to specify any imports that need to be included in the component. + - The `position` attribute of the `VarData` object can be set to `Hooks.HookPosition.PRE_TRIGGER` or `Hooks.HookPosition.POST_TRIGGER` to specify the position of the hook in the component. + +```md alert info +# The `position` attribute is only used for hooks that need to be written in a specific order. + +- If an event handler need to refer to a variable defined in a hook, the hook should be written before the event handler. +- If a hook need to refer to the memoized event handler by name, the hook should be written after the event handler. +``` + +```python +from reflex.vars.base import Var, VarData +from reflex.constants import Hooks +from reflex.components.el.elements import Div + +class ComponentWithHooks(Div, MyBaseComponent): + """MyComponent.""" + + def add_hooks(self) -> list[str| Var]: + """Add hooks to the component.""" + hooks = [] + hooks1 = """const customHookVariable = "some value";""" + hooks.append(hooks1) + + # A hook that need to be written before the memoized event handlers. + hooks2 = Var( + """useEffect(() => { + console.log("PreTrigger: " + customHookVariable); + }, []); + """, + _var_data=VarData( + imports=\{"react": ["useEffect"],\}, + position=Hooks.HookPosition.PRE_TRIGGER + ), + ) + hooks.append(hooks2) + + hooks3 = Var( + """useEffect(() => { + console.log("PostTrigger: " + customHookVariable); + }, []); + """, + _var_data=VarData( + imports=\{"react": ["useEffect"],\}, + position=Hooks.HookPosition.POST_TRIGGER + ), + ) + hooks.append(hooks3) + return hooks +``` + +The `ComponentWithHooks` will be rendered in the component in the following way: + +```javascript +export function Div_7178f430b7b371af8a12d8265d65ab9b() { + const customHookVariable = "some value"; + + useEffect(() => { + console.log("PreTrigger: " + customHookVariable); + }, []); + + /* memoized triggers such as on_click, on_change, etc will render here */ + + useEffect(() => { + console.log("PostTrigger: "+ customHookVariable); + }, []); + + return jsx("div", \{\}); +} +``` + +```md alert info +# You can mix custom code and hooks in the same component. Hooks can access a variable defined in the custom code, but custom code cannot access a variable defined in a hook. +``` diff --git a/docs/wrapping-react/example.md b/docs/wrapping-react/example.md new file mode 100644 index 00000000000..5cb34ace0a6 --- /dev/null +++ b/docs/wrapping-react/example.md @@ -0,0 +1,408 @@ +```python exec +import reflex as rx +from typing import Any +``` + +# Complex Example + +In this more complex example we will be wrapping `reactflow` a library for building node based applications like flow charts, diagrams, graphs, etc. + +## Import + +Lets start by importing the library [reactflow](https://www.npmjs.com/package/reactflow). Lets make a separate file called `reactflow.py` and add the following code: + +```python +import reflex as rx +from typing import Any, Dict, List, Union + +class ReactFlowLib(rx.Component): + """A component that wraps a react flow lib.""" + + library = "reactflow" + + def _get_custom_code(self) -> str: + return """import 'reactflow/dist/style.css'; + """ +``` + +Notice we also use the `_get_custom_code` method to import the css file that is needed for the styling of the library. + +## Components + +For this tutorial we will wrap three components from Reactflow: `ReactFlow`, `Background`, and `Controls`. Lets start with the `ReactFlow` component. + +Here we will define the `tag` and the `vars` that we will need to use the component. + +For this tutorial we will define `EventHandler` props `on_nodes_change` and `on_connect`, but you can find all the events that the component triggers in the [reactflow docs](https://reactflow.dev/docs/api/react-flow-props/#onnodeschange). + +```python +import reflex as rx +from typing import Any, Dict, List, Union + +class ReactFlowLib(rx.Component): + ... + +class ReactFlow(ReactFlowLib): + + tag = "ReactFlow" + + nodes: rx.Var[List[Dict[str, Any]]] + + edges: rx.Var[List[Dict[str, Any]]] + + fit_view: rx.Var[bool] + + nodes_draggable: rx.Var[bool] + + nodes_connectable: rx.Var[bool] + + nodes_focusable: rx.Var[bool] + + on_nodes_change: rx.EventHandler[lambda e0: [e0]] + + on_connect: rx.EventHandler[lambda e0: [e0]] +``` + +Now lets add the `Background` and `Controls` components. We will also create the components using the `create` method so that we can use them in our app. + +```python +import reflex as rx +from typing import Any, Dict, List, Union + +class ReactFlowLib(rx.Component): + ... + +class ReactFlow(ReactFlowLib): + ... + +class Background(ReactFlowLib): + + tag = "Background" + + color: rx.Var[str] + + gap: rx.Var[int] + + size: rx.Var[int] + + variant: rx.Var[str] + +class Controls(ReactFlowLib): + + tag = "Controls" + +react_flow = ReactFlow.create +background = Background.create +controls = Controls.create +``` + +## Building the App + +Now that we have our components lets build the app. + +Lets start by defining the initial nodes and edges that we will use in our app. + +```python +import reflex as rx +from .react_flow import react_flow, background, controls +import random +from collections import defaultdict +from typing import Any, Dict, List + + +initial_nodes = [ + \{ + 'id': '1', + 'type': 'input', + 'data': \{'label': '150'}, + 'position': \{'x': 250, 'y': 25}, + }, + \{ + 'id': '2', + 'data': \{'label': '25'}, + 'position': \{'x': 100, 'y': 125}, + }, + \{ + 'id': '3', + 'type': 'output', + 'data': \{'label': '5'}, + 'position': \{'x': 250, 'y': 250}, + }, +] + +initial_edges = [ + \{'id': 'e1-2', 'source': '1', 'target': '2', 'label': '*', 'animated': True}, + \{'id': 'e2-3', 'source': '2', 'target': '3', 'label': '+', 'animated': True}, +] +``` + +Next we will define the state of our app. We have four event handlers: `add_random_node`, `clear_graph`, `on_connect` and `on_nodes_change`. + +The `on_nodes_change` event handler is triggered when a node is selected and dragged. This function is used to update the position of a node during dragging. It takes a single argument `node_changes`, which is a list of dictionaries containing various types of metadata. For updating positions, the function specifically processes changes of type `position`. + +```python +class State(rx.State): + """The app state.""" + nodes: List[Dict[str, Any]] = initial_nodes + edges: List[Dict[str, Any]] = initial_edges + + @rx.event + def add_random_node(self): + new_node_id = f'\{len(self.nodes) + 1\}' + node_type = random.choice(['default']) + # Label is random number + label = new_node_id + x = random.randint(0, 500) + y = random.randint(0, 500) + + new_node = { + 'id': new_node_id, + 'type': node_type, + 'data': \{'label': label}, + 'position': \{'x': x, 'y': y}, + 'draggable': True, + } + self.nodes.append(new_node) + + @rx.event + def clear_graph(self): + self.nodes = [] # Clear the nodes list + self.edges = [] # Clear the edges list + + @rx.event + def on_connect(self, new_edge): + # Iterate over the existing edges + for i, edge in enumerate(self.edges): + # If we find an edge with the same ID as the new edge + if edge["id"] == f"e\{new_edge['source']}-\{new_edge['target']}": + # Delete the existing edge + del self.edges[i] + break + + # Add the new edge + self.edges.append({ + "id": f"e\{new_edge['source']}-\{new_edge['target']}", + "source": new_edge["source"], + "target": new_edge["target"], + "label": random.choice(["+", "-", "*", "/"]), + "animated": True, + }) + + @rx.event + def on_nodes_change(self, node_changes: List[Dict[str, Any]]): + # Receives a list of Nodes in case of events like dragging + map_id_to_new_position = defaultdict(dict) + + # Loop over the changes and store the new position + for change in node_changes: + if change["type"] == "position" and change.get("dragging") == True: + map_id_to_new_position[change["id"]] = change["position"] + + # Loop over the nodes and update the position + for i, node in enumerate(self.nodes): + if node["id"] in map_id_to_new_position: + new_position = map_id_to_new_position[node["id"]] + self.nodes[i]["position"] = new_position +``` + +Now lets define the UI of our app. We will use the `react_flow` component and pass in the `nodes` and `edges` from our state. We will also add the `on_connect` event handler to the `react_flow` component to handle when an edge is connected. + +```python +def index() -> rx.Component: + return rx.vstack( + react_flow( + background(), + controls(), + nodes_draggable=True, + nodes_connectable=True, + on_connect=lambda e0: State.on_connect(e0), + on_nodes_change=lambda e0: State.on_nodes_change(e0), + nodes=State.nodes, + edges=State.edges, + fit_view=True, + ), + rx.hstack( + rx.button("Clear graph", on_click=State.clear_graph, width="100%"), + rx.button("Add node", on_click=State.add_random_node, width="100%"), + width="100%", + ), + height="30em", + width="100%", + ) + + +# Add state and page to the app. +app = rx.App() +app.add_page(index) +``` + +```python exec +import reflex as rx +from typing import Any, Dict, List, Union +from collections import defaultdict +import random + +class ReactFlowLib(rx.Component): + """A component that wraps a react flow lib.""" + + library = "reactflow" + + def _get_custom_code(self) -> str: + return """import 'reactflow/dist/style.css'; + """ + +class ReactFlow(ReactFlowLib): + + tag = "ReactFlow" + + nodes: rx.Var[List[Dict[str, Any]]] + + edges: rx.Var[List[Dict[str, Any]]] + + fit_view: rx.Var[bool] + + nodes_draggable: rx.Var[bool] + + nodes_connectable: rx.Var[bool] + + nodes_focusable: rx.Var[bool] + + on_nodes_change: rx.EventHandler[lambda e0: [e0]] + + on_connect: rx.EventHandler[lambda e0: [e0]] + + +class Background(ReactFlowLib): + + tag = "Background" + + color: rx.Var[str] + + gap: rx.Var[int] + + size: rx.Var[int] + + variant: rx.Var[str] + +class Controls(ReactFlowLib): + + tag = "Controls" + +react_flow = ReactFlow.create +background = Background.create +controls = Controls.create + +initial_nodes = [ + { + 'id': '1', + 'type': 'input', + 'data': {'label': '150'}, + 'position': {'x': 250, 'y': 25}, + }, + { + 'id': '2', + 'data': {'label': '25'}, + 'position': {'x': 100, 'y': 125}, + }, + { + 'id': '3', + 'type': 'output', + 'data': {'label': '5'}, + 'position': {'x': 250, 'y': 250}, + }, +] + +initial_edges = [ + {'id': 'e1-2', 'source': '1', 'target': '2', 'label': '*', 'animated': True}, + {'id': 'e2-3', 'source': '2', 'target': '3', 'label': '+', 'animated': True}, +] + + +class ReactFlowState(rx.State): + """The app state.""" + nodes: List[Dict[str, Any]] = initial_nodes + edges: List[Dict[str, Any]] = initial_edges + + @rx.event + def add_random_node(self): + new_node_id = f'{len(self.nodes) + 1}' + node_type = random.choice(['default']) + # Label is random number + label = new_node_id + x = random.randint(0, 250) + y = random.randint(0, 250) + + new_node = { + 'id': new_node_id, + 'type': node_type, + 'data': {'label': label}, + 'position': {'x': x, 'y': y}, + 'draggable': True, + } + self.nodes.append(new_node) + + @rx.event + def clear_graph(self): + self.nodes = [] # Clear the nodes list + self.edges = [] # Clear the edges list + + @rx.event + def on_connect(self, new_edge): + # Iterate over the existing edges + for i, edge in enumerate(self.edges): + # If we find an edge with the same ID as the new edge + if edge["id"] == f"e{new_edge['source']}-{new_edge['target']}": + # Delete the existing edge + del self.edges[i] + break + + # Add the new edge + self.edges.append({ + "id": f"e{new_edge['source']}-{new_edge['target']}", + "source": new_edge["source"], + "target": new_edge["target"], + "label": random.choice(["+", "-", "*", "/"]), + "animated": True, + }) + + @rx.event + def on_nodes_change(self, node_changes: List[Dict[str, Any]]): + # Receives a list of Nodes in case of events like dragging + map_id_to_new_position = defaultdict(dict) + + # Loop over the changes and store the new position + for change in node_changes: + if change["type"] == "position" and change.get("dragging") == True: + map_id_to_new_position[change["id"]] = change["position"] + + # Loop over the nodes and update the position + for i, node in enumerate(self.nodes): + if node["id"] in map_id_to_new_position: + new_position = map_id_to_new_position[node["id"]] + self.nodes[i]["position"] = new_position +``` + +Here is an example of the app running: + +```python eval +rx.vstack( + react_flow( + background(), + controls(), + nodes_draggable=True, + nodes_connectable=True, + on_connect=lambda e0: ReactFlowState.on_connect(e0), + on_nodes_change=lambda e0: ReactFlowState.on_nodes_change(e0), + nodes=ReactFlowState.nodes, + edges=ReactFlowState.edges, + fit_view=True, + ), + rx.hstack( + rx.button("Clear graph", on_click=ReactFlowState.clear_graph, width="50%"), + rx.button("Add node", on_click=ReactFlowState.add_random_node, width="50%"), + width="100%", + ), + height="30em", + width="100%", + ) +``` diff --git a/docs/wrapping-react/imports-and-styles.md b/docs/wrapping-react/imports-and-styles.md new file mode 100644 index 00000000000..eb3648cf85c --- /dev/null +++ b/docs/wrapping-react/imports-and-styles.md @@ -0,0 +1,50 @@ +# Styles and Imports + +When wrapping a React component, you may need to define styles and imports that are specific to the component. This is done by defining the `add_styles` and `add_imports` methods in your component class. + +### Imports + +Sometimes, the component you are wrapping will need to import other components or libraries. This is done by defining the `add_imports` method in your component class. +That method should return a dictionary of imports, where the keys are the names of the packages to import and the values are the names of the components or libraries to import. + +Values can be either a string or a list of strings. If the import needs to be aliased, you can use the `ImportVar` object to specify the alias and whether the import should be installed as a dependency. + +```python +from reflex.utils.imports import ImportVar + +class ComponentWithImports(MyBaseComponent): + def add_imports(self): + """Add imports to the component.""" + return { + # If you only have one import, you can use a string. + "my-package1": "my-import1", + # If you have multiple imports, you can pass a list. + "my-package2": ["my-import2"], + # If you need to control the import in a more detailed way, you can use an ImportVar object. + "my-package3": ImportVar(tag="my-import3", alias="my-alias", install=False, is_default=False), + # To import a CSS file, pass the full path to the file, and use an empty string as the key. + "": "my-package-with-css/styles.css", + } +``` + +```md alert info +# The tag and library of the component will be automatically added to the imports. They do not need to be added again in `add_imports`. +``` + +### Styles + +Styles are any CSS styles that need to be included in the component. The style will be added inline to the component, so you can use any CSS styles that are valid in React. + +```python +class StyledComponent(MyBaseComponent): + """MyComponent.""" + + def add_style(self) -> dict[str, Any] | None: + """Add styles to the component.""" + + return rx.Style({ + "backgroundColor": "red", + "color": "white", + "padding": "10px", + }) +``` diff --git a/docs/wrapping-react/library-and-tags.md b/docs/wrapping-react/library-and-tags.md new file mode 100644 index 00000000000..75e2d45935c --- /dev/null +++ b/docs/wrapping-react/library-and-tags.md @@ -0,0 +1,169 @@ +--- +title: Library and Tags +--- + +```python exec +from pcweb.pages.docs import api_reference +``` + +# Find The Component + +There are two ways to find a component to wrap: + +1. Write the component yourself locally. +2. Find a well-maintained React library on [npm](https://www.npmjs.com/) that contains the component you need. + +In both cases, the process of wrapping the component is the same except for the `library` field. + +# Wrapping the Component + +To start wrapping your React component, the first step is to create a new component in your Reflex app. This is done by creating a new class that inherits from `rx.Component` or `rx.NoSSRComponent`. + +See the [API Reference]({api_reference.component.path}) for more details on the `rx.Component` class. + +This is when we will define the most important attributes of the component: + +1. **library**: The name of the npm package that contains the component. +2. **tag**: The name of the component to import from the package. +3. **alias**: (Optional) The name of the alias to use for the component. This is useful if multiple component from different package have a name in common. If `alias` is not specified, `tag` will be used. +4. **lib_dependencies**: Any additional libraries needed to use the component. +5. **is_default**: (Optional) If the component is a default export from the module, set this to `True`. Default is `False`. + +Optionally, you can override the default component creation behavior by implementing the `create` class method. Most components won't need this when props are straightforward conversions from Python to JavaScript. However, this is useful when you need to add custom initialization logic, transform props, or handle special cases when the component is created. + +```md alert warning +# When setting the `library` attribute, it is recommended to included a pinned version of the package. Doing so, the package will only change when you intentionally update the version, avoid unexpected breaking changes. +``` + +```python +class MyBaseComponent(rx.Component): + """MyBaseComponent.""" + + # The name of the npm package. + library = "my-library@x.y.z" + + # The name of the component to use from the package. + tag = "MyComponent" + + # Any additional libraries needed to use the component. + lib_dependencies: list[str] = ["package-deps@x.y.z"] + + # The name of the alias to use for the component. + alias = "MyComponentAlias" + + # If the component is a default export from the module, set this to True. + is_default = True/False + + @classmethod + def create(cls, *children, **props): + """Create an instance of MyBaseComponent. + + Args: + *children: The children of the component. + **props: The props of the component. + + Returns: + The component instance. + """ + # Your custom creation logic here + return super().create(*children, **props) + +``` + +# Wrapping a Dynamic Component + +When wrapping some libraries, you may want to use dynamic imports. This is because they may not be compatible with Server-Side Rendering (SSR). + +To handle this in Reflex, subclass `NoSSRComponent` when defining your component. It works the same as `rx.Component`, but it will automatically add the correct custom code for a dynamic import. + +Often times when you see an import something like this: + +```javascript +import dynamic from "next/dynamic"; + +const MyLibraryComponent = dynamic(() => import("./MyLibraryComponent"), { + ssr: false, +}); +``` + +You can wrap it in Reflex like this: + +```python +from reflex.components.component import NoSSRComponent + +class MyLibraryComponent(NoSSRComponent): + """A component that wraps a lib needing dynamic import.""" + + library = "my-library@x.y.z" + + tag="MyLibraryComponent" +``` + +It may not always be clear when a library requires dynamic imports. A few things to keep in mind are if the component is very client side heavy i.e. the view and structure depends on things that are fetched at run time, or if it uses `window` or `document` objects directly it will need to be wrapped as a `NoSSRComponent`. + +Some examples are: + +1. Video and Audio Players +2. Maps +3. Drawing Canvas +4. 3D Graphics +5. QR Scanners +6. Reactflow + +The reason for this is that it does not make sense for your server to render these components as the server does not have access to your camera, it cannot draw on your canvas or render a video from a file. + +In addition, if in the component documentation it mentions nextJS compatibility or server side rendering compatibility, it is a good sign that it requires dynamic imports. + +# Advanced - Parsing a state Var with a JS Function + +When wrapping a component, you may need to parse a state var by applying a JS function to it. + +## Define the parsing function + +First you need to define the parsing function by writing it in `add_custom_code`. + +```python + +def add_custom_code(self) -> list[str]: + """Add custom code to the component.""" + # Define the parsing function + return [ + """ + function myParsingFunction(inputProp) { + // Your parsing logic here + return parsedProp; + }""" + ] +``` + +## Apply the parsing function to your props + +Then, you can apply the parsing function to your props in the `create` method. + +```python +from reflex.vars.base import Var +from reflex.vars.function import FunctionStringVar + + ... + @classmethod + def create(cls, *children, **props): + """Create an instance of MyBaseComponent. + + Args: + *children: The children of the component. + **props: The props of the component. + + Returns: + The component instance. + """ + # Apply the parsing function to the props + if (prop_to_parse := props.get("propsToParse")) is not None: + if isinstance(prop_to_parse, Var): + props["propsToParse"] = FunctionStringVar.create("myParsingFunction").call(prop_to_parse) + else: + # This is not a state Var, so you can parse the value directly in python + parsed_prop = python_parsing_function(prop_to_parse) + props["propsToParse"] = parsed_prop + return super().create(*children, **props) + ... +``` diff --git a/docs/wrapping-react/local-packages.md b/docs/wrapping-react/local-packages.md new file mode 100644 index 00000000000..d7555c10517 --- /dev/null +++ b/docs/wrapping-react/local-packages.md @@ -0,0 +1,171 @@ +--- +title: Wrapping Local Packages +--- + +```python exec +import reflex as rx +``` + +# Assets + +If a wrapped component depends on assets such as images, scripts, or +stylesheets, these can be kept adjacent to the component code and +included in the final build using the `rx.asset` function. + +`rx.asset` returns a relative path that references the asset in the compiled +output. The target files are copied into a subdirectory of `assets/external` +based on the module where they are initially used. This allows third-party +components to have external assets with the same name without conflicting +with each other. + +For example, if there is an SVG file named `wave.svg` in the same directory as +this component, it can be rendered using `rx.image` and `rx.asset`. + +```python +class Hello(rx.Component): + @classmethod + def create(cls, *children, **props) -> rx.Component: + props.setdefault("align", "center") + return rx.hstack( + rx.image(src=rx.asset("wave.svg", shared=True), width="50px", height="50px"), + rx.heading("Hello ", *children), + **props + ) +``` + +# Local Components + +You can also wrap components that you have written yourself. For local components (when the code source is directly in the project), we recommend putting it beside the files that is wrapping it. + +If there is a file `hello.jsx` in the same directory as the component with this content: + +```javascript +// /path/to/components/hello.jsx +import React from "react"; + +export function Hello({ name, onGreet }) { + return ( +
+

Hello, {name}!

+ +
+ ); +} +``` + +The python app can use the `rx.asset` helper to copy the component source into +the generated frontend, after which the `library` path in the `rx.Component` may +be specified by prefixing `$/public` to that path returned by `rx.asset`. + +```python +import reflex as rx + +hello_path = rx.asset("./hello.jsx", shared=True) +hello_css_path = rx.asset("./hello.css", shared=True) + +class Hello(rx.Component): + # Use an absolute path starting with $/public + library = f"$/public{hello_path}" + + # Define everything else as normal. + tag = "Hello" + + name: rx.Var[str] = rx.Var.create("World") + on_greet: rx.EventHandler[rx.event.passthrough_event_spec(str)] + + # Include any related CSS files with rx.asset to ensure they are copied. + def add_imports(self): + return {"": f"$/public/{hello_css_path}"} +``` + +## Considerations + +When wrapping local components, keep the following in mind: + +1. **File Extensions**: Ensure that the file extensions are correct (e.g., `.jsx` for React components and `.tsx` for TypeScript components). +2. **Asset Management**: Use `rx.asset` with `shared=True` to manage any assets (e.g., images, styles) that the component depends on. +3. **Event Handling**: Define any event handlers (e.g., `on_greet`) as part of the component's API and pass those to the component _from the Reflex app_. Do not attempt to hook into Reflex's event system directly from Javascript. + +## Use Case + +Local components are useful when shimming small pieces of functionality that are +simpler or more performant when implemented directly in Javascript, such as: + +- Spammy events: keys, touch, mouse, scroll -- these are often better processed on the client side. +- Using canvas, graphics or WebGPU +- Working with other Web APIs like storage, screen capture, audio/midi +- Integrating with complex third-party libraries + - For application-specific use, it may be easier to wrap a local component that + provides the needed subset of the library's functionality in a simpler API for use in Reflex. + +# Local Packages + +If the component is part of a local package, available on Github, or +downloadable via a web URL, it can also be wrapped in Reflex. Specify the path +or URL after an `@` following the package name. + +Any local paths are relative to the `.web` folder, so you can use `../` prefix +to reference the Reflex project root. + +Some examples of valid specifiers for a package called +[`@masenf/hello-react`](https://github.com/masenf/hello-react) are: + +- GitHub: `@masenf/hello-react@github:masenf/hello-react` +- URL: `@masenf/hello-react@https://github.com/masenf/hello-react/archive/refs/heads/main.tar.gz` +- Local Archive: `@masenf/hello-react@../hello-react.tgz` +- Local Directory: `@masenf/hello-react@../hello-react` + +It is important that the package name matches the name in `package.json` so +Reflex can generate the correct import statement in the generated javascript +code. + +These package specifiers can be used for `library` or `lib_dependencies`. + +```python demo exec toggle +class GithubComponent(rx.Component): + library = "@masenf/hello-react@github:masenf/hello-react" + tag = "Counter" + + def add_imports(self): + return { + "": ["@masenf/hello-react/dist/style.css"] + } + +def github_component_example(): + return GithubComponent.create() +``` + +Although more complicated, this approach is useful when the local components +have additional dependencies or build steps required to prepare the component +for use. + +Some important notes regarding this approach: + +- The repo or archive must contain a `package.json` file. +- `prepare` or `build` scripts will NOT be executed. The distribution archive, + directory, or repo must already contain the built javascript files (this is common). + +````md alert +# Ensure CSS files are exported in `package.json` + +In addition to exporting the module containing the component, any CSS files +intended to be imported by the wrapped component must also be listed in the +`exports` key of `package.json`. + +```json +{ + // ..., + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.umd.cjs" + }, + "./dist/style.css": { + "import": "./dist/style.css", + "require": "./dist/style.css" + } + } + // ... +} +``` +```` diff --git a/docs/wrapping-react/more-wrapping-examples.md b/docs/wrapping-react/more-wrapping-examples.md new file mode 100644 index 00000000000..8fc11d855d8 --- /dev/null +++ b/docs/wrapping-react/more-wrapping-examples.md @@ -0,0 +1,447 @@ +# More React Libraries + +## AG Charts + +Here we wrap the AG Charts library from the NPM package [ag-charts-react](https://www.npmjs.com/package/ag-charts-react). + +In the react code below we can see the first `2` lines are importing React and ReactDOM, and this can be ignored when wrapping your component. + +We import the `AgCharts` component from the `ag-charts-react` library on line 5. In Reflex this is wrapped by `library = "ag-charts-react"` and `tag = "AgCharts"`. + +Line `7` defines a functional React component, which on line `26` returns `AgCharts` which is similar in the Reflex code to using the `chart` component. + +Line `9` uses the `useState` hook to create a state variable `chartOptions` and its setter function `setChartOptions` (equivalent to the event handler `set_chart_options` in reflex). The initial state variable is of type dict and has two key value pairs `data` and `series`. + +When we see `useState` in React code, it correlates to state variables in your State. As you can see in our Reflex code we have a state variable `chart_options` which is a dictionary, like in our React code. + +Moving to line `26` we see that the `AgCharts` has a prop `options`. In order to use this in Reflex we must wrap this prop. We do this with `options: rx.Var[dict]` in the `AgCharts` component. + +Lines `31` and `32` are rendering the component inside the root element. This can be ignored when we are wrapping a component as it is done in Reflex by creating an `index` function and adding it to the app. + +---md tabs + +--tab React Code + +```javascript +1 | import React, \{ useState } from 'react'; +2 | import ReactDOM from 'react-dom/client'; +3 | +4 | // React Chart Component +5 | import \{ AgCharts } from 'ag-charts-react'; +6 | +7 | const ChartExample = () => { +8 | // Chart Options: Control & configure the chart +9 | const [chartOptions, setChartOptions] = useState({ +10| // Data: Data to be displayed in the chart +11| data: [ +12| \{ month: 'Jan', avgTemp: 2.3, iceCreamSales: 162000 }, +13| \{ month: 'Mar', avgTemp: 6.3, iceCreamSales: 302000 }, +14| \{ month: 'May', avgTemp: 16.2, iceCreamSales: 800000 }, +15| \{ month: 'Jul', avgTemp: 22.8, iceCreamSales: 1254000 }, +16| \{ month: 'Sep', avgTemp: 14.5, iceCreamSales: 950000 }, +17| \{ month: 'Nov', avgTemp: 8.9, iceCreamSales: 200000 }, +18| ], +19| // Series: Defines which chart type and data to use +20| series: [\{ type: 'bar', xKey: 'month', yKey: 'iceCreamSales' }], +21| }); +22| +23| // React Chart Component +24| return ( +25| // AgCharts component with options passed as prop +26| +27| ); +28| } +29| +30| // Render component inside root element +31| const root = ReactDOM.createRoot(document.getElementById('root')); +32| root.render(); +``` + +-- +--tab Reflex Code + +```python +import reflex as rx + +class AgCharts(rx.Component): + """ A simple line chart component using AG Charts """ + + library = "ag-charts-react" + + tag = "AgCharts" + + options: rx.Var[dict] + + +chart = AgCharts.create + + +class State(rx.State): + """The app state.""" + chart_options: dict = { + "data": [ + \{"month":"Jan", "avgTemp":2.3, "iceCreamSales":162000}, + \{"month":"Mar", "avgTemp":6.3, "iceCreamSales":302000}, + \{"month":"May", "avgTemp":16.2, "iceCreamSales":800000}, + \{"month":"Jul", "avgTemp":22.8, "iceCreamSales":1254000}, + \{"month":"Sep", "avgTemp":14.5, "iceCreamSales":950000}, + \{"month":"Nov", "avgTemp":8.9, "iceCreamSales":200000} + ], + "series": [\{"type":"bar", "xKey":"month", "yKey":"iceCreamSales"}] + } + +def index() -> rx.Component: + return chart( + options=State.chart_options, + ) + +app = rx.App() +app.add_page(index) +``` + +-- + +--- + +## React Leaflet + +```python exec +from pcweb.pages import docs +``` + +In this example we are wrapping the React Leaflet library from the NPM package [react-leaflet](https://www.npmjs.com/package/react-leaflet). + +On line `1` we import the `dynamic` function from Next.js and on line `21` we set `ssr: false`. Lines `4` and `6` use the `dynamic` function to import the `MapContainer` and `TileLayer` components from the `react-leaflet` library. This is used to dynamically import the `MapContainer` and `TileLayer` components from the `react-leaflet` library. This is done in Reflex by using the `NoSSRComponent` class when defining the component. There is more information of when this is needed on the `Dynamic Imports` section of this [page]({docs.wrapping_react.library_and_tags.path}). + +It mentions in the documentation that it is necessary to include the Leaflet CSS file, which is added on line `2` in the React code below. This can be done in Reflex by using the `add_imports` method in the `MapContainer` component. We can add a relative path from within the React library or a full URL to the CSS file. + +Line `4` defines a functional React component, which on line `8` returns the `MapContainer` which is done in the Reflex code using the `map_container` component. + +The `MapContainer` component has props `center`, `zoom`, `scrollWheelZoom`, which we wrap in the `MapContainer` component in the Reflex code. We ignore the `style` prop as it is a reserved name in Reflex. We can use the `rename_props` method to change the name of the prop, as we will see in the React PDF Renderer example, but in this case we just ignore it and add the `width` and `height` props as css in Reflex. + +The `TileLayer` component has a prop `url` which we wrap in the `TileLayer` component in the Reflex code. + +Lines `24` and `25` defines and exports a React functional component named `Home` which returns the `MapComponent` component. This can be ignored in the Reflex code when wrapping the component as we return the `map_container` component in the `index` function. + +---md tabs + +--tab React Code + +```javascript +1 | import dynamic from "next/dynamic"; +2 | import "leaflet/dist/leaflet.css"; +3 | +4 | const MapComponent = dynamic( +5 | () => { +6 | return import("react-leaflet").then((\{ MapContainer, TileLayer }) => { +7 | return () => ( +8 | +14| +17| +18| ); +19| }); +20| }, +21| \{ ssr: false } +22| ); +23| +24| export default function Home() { +25| return ; +26| } +``` + +-- +--tab Reflex Code + +```python +import reflex as rx + +class MapContainer(rx.NoSSRComponent): + + library = "react-leaflet" + + tag = "MapContainer" + + center: rx.Var[list] + + zoom: rx.Var[int] + + scroll_wheel_zoom: rx.Var[bool] + + # Can also pass a url like: https://unpkg.com/leaflet/dist/leaflet.css + def add_imports(self): + return \{"": ["leaflet/dist/leaflet.css"]} + + + +class TileLayer(rx.NoSSRComponent): + + library = "react-leaflet" + + tag = "TileLayer" + + url: rx.Var[str] + + +map_container = MapContainer.create +tile_layer = TileLayer.create + +def index() -> rx.Component: + return map_container( + tile_layer(url="https://\{s}.tile.openstreetmap.org/\{z}/\{x}/\{y}.png"), + center=[51.505, -0.09], + zoom=13, + #scroll_wheel_zoom=True + width="100%", + height="50vh", + ) + + +app = rx.App() +app.add_page(index) + +``` + +-- + +--- + +## React PDF Renderer + +In this example we are wrapping the React renderer for creating PDF files on the browser and server from the NPM package [@react-pdf/renderer](https://www.npmjs.com/package/@react-pdf/renderer). + +This example is similar to the previous examples, and again Dynamic Imports are required for this library. This is done in Reflex by using the `NoSSRComponent` class when defining the component. There is more information on why this is needed on the `Dynamic Imports` section of this [page]({docs.wrapping_react.library_and_tags.path}). + +The main difference with this example is that the `style` prop, used on lines `20`, `21` and `24` in React code, is a reserved name in Reflex so can not be wrapped. A different name must be used when wrapping this prop and then this name must be changed back to the original with the `rename_props` method. In this example we name the prop `theme` in our Reflex code and then change it back to `style` with the `rename_props` method in both the `Page` and `View` components. + +```md alert info +# List of reserved names in Reflex + +_The style of the component._ + +`style: Style = Style()` + +_A mapping from event triggers to event chains._ + +`event_triggers: Dict[str, Union[EventChain, Var]] = \{}` + +_The alias for the tag._ + +`alias: Optional[str] = None` + +_Whether the import is default or named._ + +`is_default: Optional[bool] = False` + +_A unique key for the component._ + +`key: Any = None` + +_The id for the component._ + +`id: Any = None` + +_The class name for the component._ + +`class_name: Any = None` + +_Special component props._ + +`special_props: List[Var] = []` + +_Whether the component should take the focus once the page is loaded_ + +`autofocus: bool = False` + +_components that cannot be children_ + +`_invalid_children: List[str] = []` + +_only components that are allowed as children_ + +`_valid_children: List[str] = []` + +_only components that are allowed as parent_ + +`_valid_parents: List[str] = []` + +_props to change the name of_ + +`_rename_props: Dict[str, str] = \{}` + +_custom attribute_ + +`custom_attrs: Dict[str, Union[Var, str]] = \{}` + +_When to memoize this component and its children._ + +`_memoization_mode: MemoizationMode = MemoizationMode()` + +_State class associated with this component instance_ + +`State: Optional[Type[reflex.state.State]] = None` +``` + +---md tabs + +--tab React Code + +```javascript +1 | import ReactDOM from 'react-dom'; +2 | import \{ Document, Page, Text, View, StyleSheet, PDFViewer } from '@react-pdf/renderer'; +3 | +4 | // Create styles +5 | const styles = StyleSheet.create({ +6 | page: { +7 | flexDirection: 'row', +8 | backgroundColor: '#E4E4E4', +9 | }, +10| section: { +11| margin: 10, +12| padding: 10, +13| flexGrow: 1, +14| }, +15| }); +16| +17| // Create Document Component +18| const MyDocument = () => ( +19| +20| +21| +22| Section #1 +23| +24| +25| Section #2 +26| +27| +28| +29| ); +30| +31| const App = () => ( +32| +33| +34| +35| ); +36| +37| ReactDOM.render(, document.getElementById('root')); +``` + +-- +--tab Reflex Code + +```python +import reflex as rx + +class Document(rx.Component): + + library = "@react-pdf/renderer" + + tag = "Document" + + +class Page(rx.Component): + + library = "@react-pdf/renderer" + + tag = "Page" + + size: rx.Var[str] + # here we are wrapping style prop but as style is a reserved name in Reflex we must name it something else and then change this name with rename props method + theme: rx.Var[dict] + + _rename_props: dict[str, str] = { + "theme": "style", + } + + +class Text(rx.Component): + + library = "@react-pdf/renderer" + + tag = "Text" + + +class View(rx.Component): + + library = "@react-pdf/renderer" + + tag = "View" + + # here we are wrapping style prop but as style is a reserved name in Reflex we must name it something else and then change this name with rename props method + theme: rx.Var[dict] + + _rename_props: dict[str, str] = { + "theme": "style", + } + + +class StyleSheet(rx.Component): + + library = "@react-pdf/renderer" + + tag = "StyleSheet" + + page: rx.Var[dict] + + section: rx.Var[dict] + + +class PDFViewer(rx.NoSSRComponent): + + library = "@react-pdf/renderer" + + tag = "PDFViewer" + + +document = Document.create +page = Page.create +text = Text.create +view = View.create +style_sheet = StyleSheet.create +pdf_viewer = PDFViewer.create + + +styles = style_sheet({ + "page": { + "flexDirection": 'row', + "backgroundColor": '#E4E4E4', + }, + "section": { + "margin": 10, + "padding": 10, + "flexGrow": 1, + }, +}) + + +def index() -> rx.Component: + return pdf_viewer( + document( + page( + view( + text("Hello, World!"), + theme=styles.section, + ), + view( + text("Hello, 2!"), + theme=styles.section, + ), + size="A4", theme=styles.page), + ), + width="100%", + height="80vh", + ) + +app = rx.App() +app.add_page(index) +``` + +-- + +--- diff --git a/docs/wrapping-react/overview.md b/docs/wrapping-react/overview.md new file mode 100644 index 00000000000..bb7f4d3a93d --- /dev/null +++ b/docs/wrapping-react/overview.md @@ -0,0 +1,151 @@ +```python exec +import reflex as rx +from typing import Any +from pcweb.components.spline import spline +from pcweb.pages.docs import custom_components +from pcweb import constants +``` + +# Wrapping React + +One of Reflex's most powerful features is the ability to wrap React components and take advantage of the vast ecosystem of React libraries. + +If you want a specific component for your app but Reflex doesn't provide it, there's a good chance it's available as a React component. Search for it on [npm]({constants.NPMJS_URL}), and if it's there, you can use it in your Reflex app. You can also create your own local React components and wrap them in Reflex. + +Once you wrap your component, you [publish it]({custom_components.overview.path}) to the Reflex library so that others can use it. + +## Simple Example + +Simple components that don't have any interaction can be wrapped with just a few lines of code. + +Below we show how to wrap the [Spline]({constants.SPLINE_URL}) library can be used to create 3D scenes and animations. + +```python demo exec +import reflex as rx + +class Spline(rx.Component): + """Spline component.""" + + # The name of the npm package. + library = "@splinetool/react-spline" + + # Any additional libraries needed to use the component. + lib_dependencies: list[str] = ["@splinetool/runtime@1.5.5"] + + # The name of the component to use from the package. + tag = "Spline" + + # Spline is a default export from the module. + is_default = True + + # Any props that the component takes. + scene: rx.Var[str] + +# Convenience function to create the Spline component. +spline = Spline.create + +# Use the Spline component in your app. +def index(): + return spline(scene="https://prod.spline.design/joLpOOYbGL-10EJ4/scene.splinecode") +``` + +## ColorPicker Example + +Similar to the Spline example we start with defining the library and tag. In this case the library is `react-colorful` and the tag is `HexColorPicker`. + +We also have a var `color` which is the current color of the color picker. + +Since this component has interaction we must specify any event triggers that the component takes. The color picker has a single trigger `on_change` to specify when the color changes. This trigger takes in a single argument `color` which is the new color. + +```python exec +from reflex.components.component import NoSSRComponent + +class ColorPicker(NoSSRComponent): + library = "react-colorful" + tag = "HexColorPicker" + color: rx.Var[str] + on_change: rx.EventHandler[lambda color: [color]] + +color_picker = ColorPicker.create + +ColorPickerState = rx._x.client_state(default="#db114b", var_name="color") +``` + +```python eval +rx.box( + ColorPickerState, + rx.vstack( + rx.heading(ColorPickerState.value, color="white"), + color_picker( + on_change=ColorPickerState.set_value + ), + ), + background_color=ColorPickerState.value, + padding="5em", + border_radius="12px", + margin_bottom="1em", +) +``` + +```python +from reflex.components.component import NoSSRComponent + +class ColorPicker(NoSSRComponent): + library = "react-colorful" + tag = "HexColorPicker" + color: rx.Var[str] + on_change: rx.EventHandler[lambda color: [color]] + +color_picker = ColorPicker.create + +class ColorPickerState(rx.State): + color: str = "#db114b" + +def index(): + return rx.box( + rx.vstack( + rx.heading(ColorPickerState.color, color="white"), + color_picker( + on_change=ColorPickerState.set_color + ), + ), + background_color=ColorPickerState.color, + padding="5em", + border_radius="1em", + ) +``` + +## What Not To Wrap + +There are some libraries on npm that are not do not expose React components and therefore are very hard to wrap with Reflex. + +A library like [spline](https://www.npmjs.com/package/@splinetool/runtime) below is going to be difficult to wrap with Reflex because it does not expose a React component. + +```javascript +import \{ Application } from '@splinetool/runtime'; + +// make sure you have a canvas in the body +const canvas = document.getElementById('canvas3d'); + +// start the application and load the scene +const spline = new Application(canvas); +spline.load('https://prod.spline.design/6Wq1Q7YGyM-iab9i/scene.splinecode'); +``` + +You should look out for JSX, a syntax extension to JavaScript, which has angle brackets `(

Hello, world!

)`. If you see JSX, it's likely that the library is a React component and can be wrapped with Reflex. + +If the library does not expose a react component you need to try and find a JS React wrapper for the library, such as [react-spline](https://www.npmjs.com/package/@splinetool/react-spline). + +```javascript +import Spline from "@splinetool/react-spline"; + +export default function App() { + return ( +
+ +
+ ); +} +``` + +In the next page, we will go step by step through a more complex example of wrapping a React component. diff --git a/docs/wrapping-react/props.md b/docs/wrapping-react/props.md new file mode 100644 index 00000000000..207fa0f1fd1 --- /dev/null +++ b/docs/wrapping-react/props.md @@ -0,0 +1,205 @@ +--- +title: Props - Wrapping React +--- + +# Props + +When wrapping a React component, you want to define the props that will be accepted by the component. +This is done by defining the props and annotating them with a `rx.Var`. + +Broadly, there are three kinds of props you can encounter when wrapping a React component: + +1. **Simple Props**: These are props that are passed directly to the component. They can be of any type, including strings, numbers, booleans, and even lists or dictionaries. +2. **Callback Props**: These are props that expect to receive a function. That function will usually be called by the component as a callback. (This is different from event handlers.) +3. **Component Props**: These are props that expect to receive a components themselves. They can be used to create more complex components by composing them together. +4. **Event Handlers**: These are props that expect to receive a function that will be called when an event occurs. They are defined as `rx.EventHandler` with a signature function to define the spec of the event. + +## Simple Props + +Simple props are the most common type of props you will encounter when wrapping a React component. They are passed directly to the component and can be of any type (but most commonly strings, numbers, booleans, and structures). + +For custom types, you can use `TypedDict` to define the structure of the custom types. However, if you need the attributes to be automatically converted to camelCase once compiled in JS, you can use `rx.PropsBase` instead of `TypedDict`. + +```python +class CustomReactType(TypedDict): + """Custom React type.""" + + # Define the structure of the custom type to match the Javascript structure. + attribute1: str + attribute2: bool + attribute3: int + + +class CustomReactType2(rx.PropsBase): + """Custom React type.""" + + # Define the structure of the custom type to match the Javascript structure. + attr_foo: str # will be attrFoo in JS + attr_bar: bool # will be attrBar in JS + attr_baz: int # will be attrBaz in JS + +class SimplePropsComponent(MyBaseComponent): + """MyComponent.""" + + # Type the props according the component documentation. + + # props annotated as `string` in javascript + prop1: rx.Var[str] + + # props annotated as `number` in javascript + prop2: rx.Var[int] + + # props annotated as `boolean` in javascript + prop3: rx.Var[bool] + + # props annotated as `string[]` in javascript + prop4: rx.Var[list[str]] + + # props annotated as `CustomReactType` in javascript + props5: rx.Var[CustomReactType] + + # props annotated as `CustomReactType2` in javascript + props6: rx.Var[CustomReactType2] + + # Sometimes a props will accept multiple types. You can use `|` to specify the types. + # props annotated as `string | boolean` in javascript + props7: rx.Var[str | bool] +``` + +## Callback Props + +Callback props are used to handle events or to pass data back to the parent component. They are defined as `rx.Var` with a type of `FunctionVar` or `Callable`. + +```python +from typing import Callable +from reflex.vars.function import FunctionVar + +class CallbackPropsComponent(MyBaseComponent): + """MyComponent.""" + + # A callback prop that takes a single argument. + callback_props: rx.Var[Callable] +``` + +## Component Props + +Some components will occasionally accept other components as props, usually annotated as `ReactNode`. In Reflex, these are defined as `rx.Component`. + +```python +class ComponentPropsComponent(MyBaseComponent): + """MyComponent.""" + + # A prop that takes a component as an argument. + component_props: rx.Var[rx.Component] +``` + +## Event Handlers + +Event handlers are props that expect to receive a function that will be called when an event occurs. They are defined as `rx.EventHandler` with a signature function to define the spec of the event. + +```python +from reflex.vars.event_handler import EventHandler +from reflex.vars.function import FunctionVar +from reflex.vars.object import ObjectVar + +class InputEventType(TypedDict): + """Input event type.""" + + # Define the structure of the input event. + foo: str + bar: int + +class OutputEventType(TypedDict): + """Output event type.""" + + # Define the structure of the output event. + baz: str + qux: int + + +def custom_spec1(event: ObjectVar[InputEventType]) -> tuple[str, int]: + """Custom event spec using ObjectVar with custom type as input and tuple as output.""" + return ( + event.foo.to(str), + event.bar.to(int), + ) + +def custom_spec2(event: ObjectVar[dict]) -> tuple[Var[OutputEventType]]: + """Custom event spec using ObjectVar with dict as input and custom type as output.""" + return Var.create( + { + "baz": event["foo"], + "qux": event["bar"], + }, + ).to(OutputEventType) + +class EventHandlerComponent(MyBaseComponent): + """MyComponent.""" + + # An event handler that take no argument. + on_event: rx.EventHandler[rx.event.no_args_event_spec] + + # An event handler that takes a single string argument. + on_event_with_arg: rx.EventHandler[rx.event.passthrough_event_spec(str)] + + # An event handler specialized for input events, accessing event.target.value from the event. + on_input_change: rx.EventHandler[rx.event.input_event] + + # An event handler specialized for key events, accessing event.key from the event and provided modifiers (ctrl, alt, shift, meta). + on_key_down: rx.EventHandler[rx.event.key_event] + + # An event handler that takes a custom spec. (Event handler must expect a tuple of two values [str and int]) + on_custom_event: rx.EventHandler[custom_spec1] + + # Another event handler that takes a custom spec. (Event handler must expect a tuple of one value, being a OutputEventType) + on_custom_event2: rx.EventHandler[custom_spec2] +``` + +```md alert info +# Custom event specs have a few use case where they are particularly useful. If the event returns non-serializable data, you can filter them out so the event can be sent to the backend. You can also use them to transform the data before sending it to the backend. +``` + +### Emulating Event Handler Behavior Outside a Component + +In some instances, you may need to replicate the special behavior applied to +event handlers from outside of a component context. For example if the component +to be wrapped requires event callbacks passed in a dictionary, this can be +achieved by directly instantiating an `EventChain`. + +A real-world example of this is the `onEvents` prop of +[`echarts-for-react`](https://www.npmjs.com/package/echarts-for-react) library, +which, unlike a normal event handler, expects a mapping of event handlers like: + +```javascript + +``` + +To achieve this in Reflex, you can create an explicit `EventChain` for each +event handler: + +```python +@classmethod +def create(cls, *children, **props): + on_events = props.pop("on_events", {}) + + event_chains = {} + for event_name, handler in on_events.items(): + # Convert the EventHandler/EventSpec/lambda to an EventChain + event_chains[event_name] = rx.EventChain.create( + handler, + args_spec=rx.event.no_args_event_spec, + key=event_name, + ) + if on_events: + props["on_events"] = event_chains + + # Create the component instance + return super().create(*children, **props) +``` diff --git a/docs/wrapping-react/serializers.md b/docs/wrapping-react/serializers.md new file mode 100644 index 00000000000..4bb1abcffd0 --- /dev/null +++ b/docs/wrapping-react/serializers.md @@ -0,0 +1,44 @@ +--- +title: Serializers +--- + +# Serializers + +Vars can be any type that can be serialized to JSON. This includes primitive types like strings, numbers, and booleans, as well as more complex types like lists, dictionaries, and dataframes. + +In case you need to serialize a more complex type, you can use the `serializer` decorator to convert the type to a primitive type that can be stored in the state. Just define a method that takes the complex type as an argument and returns a primitive type. We use type annotations to determine the type that you want to serialize. + +For example, the Plotly component serializes a plotly figure into a JSON string that can be stored in the state. + +```python +import json +import reflex as rx +from plotly.graph_objects import Figure +from plotly.io import to_json + +# Use the serializer decorator to convert the figure to a JSON string. +# Specify the type of the argument as an annotation. +@rx.serializer +def serialize_figure(figure: Figure) -> list: + # Use Plotly's to_json method to convert the figure to a JSON string. + return json.loads(to_json(figure))["data"] +``` + +We can then define a var of this type as a prop in our component. + +```python +import reflex as rx +from plotly.graph_objects import Figure + +class Plotly(rx.Component): + """Display a plotly graph.""" + library = "react-plotly.js@2.6.0" + lib_dependencies: List[str] = ["plotly.js@2.22.0"] + + tag = "Plot" + + is_default = True + + # Since a serialize is defined now, we can use the Figure type directly. + data: rx.Var[Figure] +``` diff --git a/docs/wrapping-react/step-by-step.md b/docs/wrapping-react/step-by-step.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/zh/zh_cn/README.md b/docs/zh/zh_cn/README.md deleted file mode 100644 index e3da05f034f..00000000000 --- a/docs/zh/zh_cn/README.md +++ /dev/null @@ -1,251 +0,0 @@ -
-Reflex Logo -
- -### **✨ 使用 Python 创建高效且可自定义的网页应用程序,几秒钟内即可部署.✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) - -
- ---- - -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex 是一个使用纯 Python 构建全栈 web 应用的库。 - -关键特性: - -- **纯 Python** - 前端、后端开发全都使用 Python,不需要学习 Javascript。 -- **完整的灵活性** - Reflex 很容易上手, 并且也可以扩展到复杂的应用程序。 -- **立即部署** - 构建后,使用[单个命令](https://reflex.dev/docs/hosting/deploy-quick-start/)就能部署应用程序;或者也可以将其托管在您自己的服务器上。 - -请参阅我们的[架构页](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture)了解 Reflex 如何工作。 - -## ⚙️ 安装 - -打开一个终端并且运行(要求 Python3.10+): - -```bash -pip install reflex -``` - -## 🥳 创建您的第一个应用程序 - -安装 Reflex 的同时也会安装 `reflex` 命令行工具. - -通过创建一个新项目来测试是否安装成功(请把 my_app_name 替代为您的项目名字): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -这段命令会在新文件夹初始化一个应用程序模板. - -您可以在开发者模式下运行这个应用程序: - -```bash -reflex run -``` - -您可以看到您的应用程序运行在 http://localhost:3000. - -现在您可以在以下位置修改代码 `my_app_name/my_app_name.py`,Reflex 拥有快速刷新(fast refresh),所以您可以在保存代码后马上看到更改. - -## 🫧 范例 - -让我们来看一个例子: 创建一个使用 [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node) 进行图像生成的图形界面.为了保持范例简单,我们只使用 OpenAI API,但是您可以将其替换成本地端的 ML 模型. - -  - -
-DALL·E的前端界面, 展示了图片生成的进程 -
- -  - -这是这个范例的完整代码,只需要一个 Python 文件就可以完成! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """The app state.""" - - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True - - -def index(): - return rx.center( - rx.vstack( - rx.heading("DALL-E", font_size="1.5em"), - rx.input( - placeholder="Enter a prompt..", - on_blur=State.set_prompt, - width="25em", - ), - rx.button( - "Generate Image", - on_click=State.get_image, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# Add state and page to the app. -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## 让我们分解以上步骤. - -
-解释 DALL-E app 的前端和后端部分的区别。 -
- -### **Reflex UI** - -让我们从 UI 开始. - -```python -def index(): - return rx.center( - ... - ) -``` - -这个 `index` 函数定义了应用程序的前端. - -我们用不同的组件比如 `center`, `vstack`, `input`, 和 `button` 来创建前端, 组件之间可以相互嵌入,来创建复杂的布局. -并且您可以使用关键字参数来使用 CSS 的全部功能. - -Reflex 拥有 [60+ 个内置组件](https://reflex.dev/docs/library) 来帮助您开始创建应用程序. 我们正在积极添加组件, 但是您也可以容易的 [创建自己的组件](https://reflex.dev/docs/wrapping-react/overview/). - -### **State** - -Reflex 用 State 来渲染您的 UI. - -```python -class State(rx.State): - """The app state.""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -State 定义了所有可能会发生变化的变量(称为 vars)以及能够改变这些变量的函数. - -在这个范例中,State 由 `prompt` 和 `image_url` 组成.此外,State 还包含有两个布尔值 `processing` 和 `complete`,用于指示何时显示循环进度指示器和图像. - -### **Event Handlers** - -```python -def get_image(self): - """Get the image from the prompt.""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True -``` - -在 State 中,我们定义了称为事件处理器(event handlers)的函数,用于改变状态变量(state vars).在 Reflex 中,事件处理器是我们可以修改状态的方式.它们可以作为对用户操作的响应而被调用,例如点击一个按钮或在文本框中输入.这些操作被称为事件. - -我们的 DALL·E 应用有一个事件处理器,名为 `get_image`,它用于从 OpenAI API 获取图像.在事件处理器中使用 `yield` 将导致 UI 进行更新.否则,UI 将在事件处理器结束时进行更新. - -### **Routing** - -最后,定义我们的应用程序. - -```python -app = rx.App() -``` - -我们添加从应用程序根目录到 index 组件的路由.我们还添加了一个在页面预览或浏览器标签中显示的标题. - -```python -app.add_page(index, title="DALL-E") -``` - -您可以通过增加更多页面来创建一个多页面的应用. - -## 📑 资源 - -
- -📑 [文档](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [日志](https://reflex.dev/blog)   |   📱 [组件库](https://reflex.dev/docs/library)   |   🖼️ [模板](https://reflex.dev/templates/)   |   🛸 [部署](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ Reflex 的状态 - -Reflex 于 2022 年 12 月以 Pynecone 的名称推出. - -从 2025 年开始,[Reflex Cloud](https://cloud.reflex.dev)已经推出,为 Reflex 应用提供最佳的托管体验。我们将继续开发并实现更多功能。 - -Reflex 每周都有新功能和发布新版本! 确保您按下 :star: 收藏和 :eyes: 关注 这个 仓库来确保知道最新信息. - -## 贡献 - -我们欢迎任何大小的贡献,以下是几个好的方法来加入 Reflex 社群. - -- **加入我们的 Discord**: 我们的 [Discord](https://discord.gg/T5WSbC2YtQ) 是帮助您加入 Reflex 项目和讨论或贡献最棒的地方. -- **GitHub Discussions**: 一个来讨论您想要添加的功能或是需要澄清的事情的好地方. -- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues)是报告错误的绝佳地方,另外您可以试着解决一些现有 issue 并提交 PR. - -我们正在积极寻找贡献者,无关您的技能或经验水平. 若要贡献,请查看[CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) - -## 感谢我们所有的贡献者: - - - - - -## 授权 - -Reflex 是一个开源项目,使用 [Apache License 2.0](/LICENSE) 授权. diff --git a/docs/zh/zh_tw/README.md b/docs/zh/zh_tw/README.md deleted file mode 100644 index 606e483ee09..00000000000 --- a/docs/zh/zh_tw/README.md +++ /dev/null @@ -1,251 +0,0 @@ -
-Reflex Logo -
- -**✨ 使用 Python 建立高效且可自訂的網頁應用程式,幾秒鐘內即可部署。✨** - -[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) -![versions](https://img.shields.io/pypi/pyversions/reflex.svg) -[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) -[![PyPI Downloads](https://static.pepy.tech/badge/reflex)](https://pepy.tech/projects/reflex) -[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) - -
- ---- - -[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) - ---- - -# Reflex - -Reflex 是一個可以用純 Python 構建全端網頁應用程式的函式庫。 - -主要特色: - -- **純 Python** - 您可以用 Python 撰寫應用程式的前端和後端,無需學習 Javascript。 -- **完全靈活性** - Reflex 易於上手,但也可以擴展到複雜的應用程式。 -- **立即部署** - 構建後,只需使用[單一指令](https://reflex.dev/docs/hosting/deploy-quick-start/)即可部署您的應用程式,或在您自己的伺服器上託管。 - -請參閱我們的[架構頁面](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture)了解 Reflex 如何在底層運作。 - -## ⚙️ 安裝 - -開啟一個終端機並且執行 (需要 Python 3.10+): - -```bash -pip install reflex -``` - -## 🥳 建立你的第一個應用程式 - -安裝 Reflex 的同時也會安裝 `reflex` 命令行工具。 - -通過創建一個新專案來測試是否安裝成功。(把 my_app_name 作為新專案名稱): - -```bash -mkdir my_app_name -cd my_app_name -reflex init -``` - -此命令會初始化一個應用程式模板在你的新資料夾中。 - -你可以在開發者模式運行這個應用程式: - -```bash -reflex run -``` - -你可以看到你的應用程式運行在 http://localhost:3000。 - -現在在以下位置修改原始碼 `my_app_name/my_app_name.py`,Reflex 擁有快速刷新功能,存儲程式碼後便可立即看到改變。 - -## 🫧 範例應用程式 - -讓我們來看一個例子: 建立一個使用 DALL·E 的圖形使用者介面,為了保持範例簡單,我們只呼叫 OpenAI API,而這部份可以置換掉,改為執行成本地端的 ML 模型。 - -  - -
-A frontend wrapper for DALL·E, shown in the process of generating an image. -
- -  - -下方為該應用之完整程式碼,這一切都只需要一個 Python 檔案就能作到! - -```python -import reflex as rx -import openai - -openai_client = openai.OpenAI() - - -class State(rx.State): - """應用程式狀態""" - prompt = "" - image_url = "" - processing = False - complete = False - - def get_image(self): - """透過提示詞取得圖片""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True - - -def index(): - return rx.center( - rx.vstack( - rx.heading("DALL-E", font_size="1.5em"), - rx.input( - placeholder="Enter a prompt..", - on_blur=State.set_prompt, - width="25em", - ), - rx.button( - "Generate Image", - on_click=State.get_image, - width="25em", - loading=State.processing - ), - rx.cond( - State.complete, - rx.image(src=State.image_url, width="20em"), - ), - align="center", - ), - width="100%", - height="100vh", - ) - -# 把狀態跟頁面添加到應用程式。 -app = rx.App() -app.add_page(index, title="Reflex:DALL-E") -``` - -## 讓我們來拆解一下。 - -
-解釋 DALL-E app 的前端和後端部分的區別。 -
- -### **Reflex 使用者介面** - -讓我們從使用介面開始。 - -```python -def index(): - return rx.center( - ... - ) -``` - -這個 `index` 函式定義了應用程式的前端. - -我們用不同的元件像是 `center`, `vstack`, `input`, 和 `button` 來建立前端,元件之間可互相套入以建立出複雜的版面配置。並且您可使用關鍵字引數 _keyword args_ 運行 CSS 全部功能來設計這些元件們的樣式。 - -Reflex 擁有 [60+ 內建元件](https://reflex.dev/docs/library) 來幫助你開始建立應用程式。我們正積極添加元件,你也可以簡單地 [創建自己所屬的元件](https://reflex.dev/docs/wrapping-react/overview/)。 - -### **應用程式狀態** - -Reflex 使用應用程式狀態中的函式來渲染你的 UI。 - -```python -class State(rx.State): - """應用程式狀態""" - prompt = "" - image_url = "" - processing = False - complete = False - -``` - -應用程式狀態定義了應用程式中所有可以更改的變數及變更他們的函式 (稱為 vars)。 - -這裡的狀態由 `prompt` 和 `image_url`組成, 以及布林變數 `processing` 和 `complete` 來指示何時顯示進度條及圖片。 - -### **事件處理程序** - -```python -def get_image(self): - """透過提示詞取得圖片""" - if self.prompt == "": - return rx.window_alert("Prompt Empty") - - self.processing, self.complete = True, False - yield - response = openai_client.images.generate( - prompt=self.prompt, n=1, size="1024x1024" - ) - self.image_url = response.data[0].url - self.processing, self.complete = False, True -``` - -在應用程式狀態中,我們定義稱之為事件處理程序的函式來改變其 vars. 事件處理程序是我們用來改變 Reflex 應用程式狀態的方法。 - -當使用者動作被響應時,對應的事件處理程序就會被呼叫。點擊按鈕或是文字框輸入都是使用者動作,它們被稱之為事件。 - -我們的 DALL·E. 應用程式有一個事件處理程序 `get_image`,它透過 Open AI API 取得圖片。在事件處理程序中使用 `yield` 將讓使用者介面中途更新,若不使用的話,使用介面只能在事件處理程序結束時才更新。 - -### **路由** - -最後,我們定義我們的應用程式 app。 - -```python -app = rx.App() -``` - -添加從應用程式根目錄(root of the app) 到 index 元件的路由。 我們也添加了一個標題將會顯示在 預覽/瀏覽 分頁。 - -```python -app.add_page(index, title="DALL-E") -``` - -你可以添加更多頁面至路由藉此來建立多頁面應用程式(multi-page app) - -## 📑 資源 - -
- -📑 [Docs](https://reflex.dev/docs/getting-started/introduction)   |   🗞️ [Blog](https://reflex.dev/blog)   |   📱 [Component Library](https://reflex.dev/docs/library)   |   🖼️ [Templates](https://reflex.dev/templates/)   |   🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)   - -
- -## ✅ 產品狀態 - -Reflex 在 2022 年 12 月以 Pynecone 的名字推出。 - -自 2025 年起,[Reflex Cloud](https://cloud.reflex.dev) 已推出,為 Reflex 應用程式提供最佳的託管體驗。我們將繼續開發並實施更多功能。 - -Reflex 每周都有新功能和釋出新版本! 確保你按下 :star: 和 :eyes: watch 這個 repository 來確保知道最新資訊。 - -## 貢獻 - -我們歡迎任何大小的貢獻,以下是一些加入 Reflex 社群的好方法。 - -- **加入我們的 Discord**: 我們的 [Discord](https://discord.gg/T5WSbC2YtQ) 是獲取 Reflex 專案幫助和討論如何貢獻的最佳地方。 -- **GitHub Discussions**: 這是一個討論您想新增的功能或對於一些困惑/需要澄清事項的好方法。 -- **GitHub Issues**: 在 [Issues](https://github.com/reflex-dev/reflex/issues) 頁面報告錯誤是一個絕佳的方式。此外,您也可以嘗試解決現有 Issue 並提交 PR。 - -我們積極尋找貢獻者,不論您的技能水平或經驗如何。要貢獻,請查看 [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) - -## 感謝所有貢獻者: - - - - - -## 授權 - -Reflex 是一個開源專案且使用 [Apache License 2.0](/LICENSE) 授權。 diff --git a/packages/reflex-components-code/README.md b/packages/reflex-components-code/README.md new file mode 100644 index 00000000000..78a2013b19c --- /dev/null +++ b/packages/reflex-components-code/README.md @@ -0,0 +1,3 @@ +# reflex-components-code + +Reflex code display components. diff --git a/packages/reflex-components-code/pyproject.toml b/packages/reflex-components-code/pyproject.toml new file mode 100644 index 00000000000..e7b95abc4a0 --- /dev/null +++ b/packages/reflex-components-code/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "reflex-components-code" +dynamic = ["version"] +description = "Reflex code display components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-code-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/packages/reflex-components-code/src/reflex_components_code/__init__.py b/packages/reflex-components-code/src/reflex_components_code/__init__.py new file mode 100644 index 00000000000..e3e7851bee9 --- /dev/null +++ b/packages/reflex-components-code/src/reflex_components_code/__init__.py @@ -0,0 +1 @@ +"""Reflex code display components.""" diff --git a/reflex/components/datadisplay/code.py b/packages/reflex-components-code/src/reflex_components_code/code.py similarity index 97% rename from reflex/components/datadisplay/code.py rename to packages/reflex-components-code/src/reflex_components_code/code.py index c80172cbdb9..cbf589009b1 100644 --- a/reflex/components/datadisplay/code.py +++ b/packages/reflex-components-code/src/reflex_components_code/code.py @@ -5,12 +5,13 @@ import dataclasses from typing import ClassVar, Literal +from reflex_components_core.core.cond import color_mode_cond +from reflex_components_lucide.icon import Icon +from reflex_components_markdown.markdown import MarkdownComponentMap +from reflex_components_radix.themes.components.button import Button +from reflex_components_radix.themes.layout.box import Box + from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.cond import color_mode_cond -from reflex.components.lucide.icon import Icon -from reflex.components.markdown.markdown import MarkdownComponentMap -from reflex.components.radix.themes.components.button import Button -from reflex.components.radix.themes.layout.box import Box from reflex.constants.colors import Color from reflex.event import set_clipboard from reflex.style import Style diff --git a/reflex/components/datadisplay/shiki_code_block.py b/packages/reflex-components-code/src/reflex_components_code/shiki_code_block.py similarity index 98% rename from reflex/components/datadisplay/shiki_code_block.py rename to packages/reflex-components-code/src/reflex_components_code/shiki_code_block.py index 30984edcfd0..844eb7a1e53 100644 --- a/reflex/components/datadisplay/shiki_code_block.py +++ b/packages/reflex-components-code/src/reflex_components_code/shiki_code_block.py @@ -8,14 +8,15 @@ from dataclasses import dataclass from typing import Any, Literal +from reflex_components_core.core.colors import color +from reflex_components_core.core.cond import color_mode_cond +from reflex_components_core.el.elements.forms import Button +from reflex_components_lucide.icon import Icon +from reflex_components_markdown.markdown import MarkdownComponentMap +from reflex_components_radix.themes.layout.box import Box + from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.colors import color -from reflex.components.core.cond import color_mode_cond -from reflex.components.el.elements.forms import Button -from reflex.components.lucide.icon import Icon -from reflex.components.markdown.markdown import MarkdownComponentMap from reflex.components.props import NoExtrasAllowedProps -from reflex.components.radix.themes.layout.box import Box from reflex.event import run_script, set_clipboard from reflex.style import Style from reflex.utils.exceptions import VarTypeError diff --git a/packages/reflex-components-core/README.md b/packages/reflex-components-core/README.md new file mode 100644 index 00000000000..bedf7d6ed79 --- /dev/null +++ b/packages/reflex-components-core/README.md @@ -0,0 +1,3 @@ +# reflex-components-core + +UI components for Reflex. diff --git a/packages/reflex-components-core/pyproject.toml b/packages/reflex-components-core/pyproject.toml new file mode 100644 index 00000000000..1f7f1a023e0 --- /dev/null +++ b/packages/reflex-components-core/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "reflex-components-core" +dynamic = ["version"] +description = "UI components for Reflex." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-core-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/packages/reflex-components-core/src/reflex_components_core/__init__.py b/packages/reflex-components-core/src/reflex_components_core/__init__.py new file mode 100644 index 00000000000..87727b9eb05 --- /dev/null +++ b/packages/reflex-components-core/src/reflex_components_core/__init__.py @@ -0,0 +1,20 @@ +"""Reflex base UI components package.""" + +from __future__ import annotations + +from reflex.utils import lazy_loader + +_SUBMODULES: set[str] = { + "base", + "core", + "datadisplay", + "el", +} + +_SUBMOD_ATTRS: dict[str, list[str]] = {} + +__getattr__, __dir__, __all__ = lazy_loader.attach( + __name__, + submodules=_SUBMODULES, + submod_attrs=_SUBMOD_ATTRS, +) diff --git a/reflex/components/base/__init__.py b/packages/reflex-components-core/src/reflex_components_core/base/__init__.py similarity index 100% rename from reflex/components/base/__init__.py rename to packages/reflex-components-core/src/reflex_components_core/base/__init__.py diff --git a/reflex/components/base/app_wrap.py b/packages/reflex-components-core/src/reflex_components_core/base/app_wrap.py similarity index 88% rename from reflex/components/base/app_wrap.py rename to packages/reflex-components-core/src/reflex_components_core/base/app_wrap.py index c7cfe650544..9b244ef23eb 100644 --- a/reflex/components/base/app_wrap.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/app_wrap.py @@ -1,8 +1,8 @@ """Top-level component that wraps the entire app.""" -from reflex.components.base.fragment import Fragment from reflex.components.component import Component from reflex.vars.base import Var +from reflex_components_core.base.fragment import Fragment class AppWrap(Fragment): diff --git a/reflex/components/base/bare.py b/packages/reflex-components-core/src/reflex_components_core/base/bare.py similarity index 100% rename from reflex/components/base/bare.py rename to packages/reflex-components-core/src/reflex_components_core/base/bare.py diff --git a/reflex/components/base/body.py b/packages/reflex-components-core/src/reflex_components_core/base/body.py similarity index 64% rename from reflex/components/base/body.py rename to packages/reflex-components-core/src/reflex_components_core/base/body.py index 2327aa2d366..51fdc70d7e1 100644 --- a/reflex/components/base/body.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/body.py @@ -1,6 +1,6 @@ """Display the page body.""" -from reflex.components.el import elements +from reflex_components_core.el import elements class Body(elements.Body): diff --git a/reflex/components/base/document.py b/packages/reflex-components-core/src/reflex_components_core/base/document.py similarity index 100% rename from reflex/components/base/document.py rename to packages/reflex-components-core/src/reflex_components_core/base/document.py diff --git a/reflex/components/base/error_boundary.py b/packages/reflex-components-core/src/reflex_components_core/base/error_boundary.py similarity index 98% rename from reflex/components/base/error_boundary.py rename to packages/reflex-components-core/src/reflex_components_core/base/error_boundary.py index 5b91442cb8c..dcf3867aa2c 100644 --- a/reflex/components/base/error_boundary.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/error_boundary.py @@ -3,13 +3,13 @@ from __future__ import annotations from reflex.components.component import Component, field -from reflex.components.datadisplay.logo import svg_logo -from reflex.components.el import a, button, div, h2, hr, p, pre, svg from reflex.event import EventHandler, set_clipboard from reflex.state import FrontendEventExceptionState from reflex.vars.base import Var from reflex.vars.function import ArgsFunctionOperation from reflex.vars.object import ObjectVar +from reflex_components_core.datadisplay.logo import svg_logo +from reflex_components_core.el import a, button, div, h2, hr, p, pre, svg def on_error_spec( diff --git a/reflex/components/base/fragment.py b/packages/reflex-components-core/src/reflex_components_core/base/fragment.py similarity index 100% rename from reflex/components/base/fragment.py rename to packages/reflex-components-core/src/reflex_components_core/base/fragment.py diff --git a/reflex/components/base/link.py b/packages/reflex-components-core/src/reflex_components_core/base/link.py similarity index 94% rename from reflex/components/base/link.py rename to packages/reflex-components-core/src/reflex_components_core/base/link.py index 7af286e49a1..4df4deeebb6 100644 --- a/reflex/components/base/link.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/link.py @@ -1,8 +1,8 @@ """Display the title of the current page.""" from reflex.components.component import field -from reflex.components.el.elements.base import BaseHTML from reflex.vars.base import Var +from reflex_components_core.el.elements.base import BaseHTML class RawLink(BaseHTML): diff --git a/reflex/components/base/meta.py b/packages/reflex-components-core/src/reflex_components_core/base/meta.py similarity index 85% rename from reflex/components/base/meta.py rename to packages/reflex-components-core/src/reflex_components_core/base/meta.py index a8f81da18ea..947e68f4e53 100644 --- a/reflex/components/base/meta.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/meta.py @@ -2,11 +2,13 @@ from __future__ import annotations -from reflex.components.base.bare import Bare from reflex.components.component import field -from reflex.components.el import elements -from reflex.components.el.elements.metadata import Meta as Meta # for compatibility from reflex.vars.base import Var +from reflex_components_core.base.bare import Bare +from reflex_components_core.el import elements +from reflex_components_core.el.elements.metadata import ( + Meta as Meta, +) # for compatibility class Title(elements.Title): diff --git a/reflex/components/base/script.py b/packages/reflex-components-core/src/reflex_components_core/base/script.py similarity index 95% rename from reflex/components/base/script.py rename to packages/reflex-components-core/src/reflex_components_core/base/script.py index 507e6db324e..dcd121d023c 100644 --- a/reflex/components/base/script.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/script.py @@ -2,9 +2,9 @@ from __future__ import annotations -from reflex.components import el as elements -from reflex.components.core.helmet import helmet from reflex.utils import console +from reflex_components_core import el as elements +from reflex_components_core.core.helmet import helmet class Script(elements.Script): diff --git a/reflex/components/base/strict_mode.py b/packages/reflex-components-core/src/reflex_components_core/base/strict_mode.py similarity index 100% rename from reflex/components/base/strict_mode.py rename to packages/reflex-components-core/src/reflex_components_core/base/strict_mode.py diff --git a/reflex/components/core/__init__.py b/packages/reflex-components-core/src/reflex_components_core/core/__init__.py similarity index 100% rename from reflex/components/core/__init__.py rename to packages/reflex-components-core/src/reflex_components_core/core/__init__.py diff --git a/reflex/components/core/auto_scroll.py b/packages/reflex-components-core/src/reflex_components_core/core/auto_scroll.py similarity index 98% rename from reflex/components/core/auto_scroll.py rename to packages/reflex-components-core/src/reflex_components_core/core/auto_scroll.py index a4eb6f56725..0ef2fcdb41e 100644 --- a/reflex/components/core/auto_scroll.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/auto_scroll.py @@ -4,10 +4,10 @@ import dataclasses -from reflex.components.el.elements.typography import Div from reflex.constants.compiler import MemoizationDisposition, MemoizationMode from reflex.utils.imports import ImportDict from reflex.vars.base import Var, get_unique_variable_name +from reflex_components_core.el.elements.typography import Div class AutoScroll(Div): diff --git a/reflex/components/core/banner.py b/packages/reflex-components-core/src/reflex_components_core/core/banner.py similarity index 97% rename from reflex/components/core/banner.py rename to packages/reflex-components-core/src/reflex_components_core/core/banner.py index 8160faf38c5..eb6f9cac585 100644 --- a/reflex/components/core/banner.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/banner.py @@ -2,20 +2,18 @@ from __future__ import annotations -from reflex import constants -from reflex.components.base.fragment import Fragment -from reflex.components.component import Component -from reflex.components.core.cond import cond -from reflex.components.el.elements.typography import Div -from reflex.components.lucide.icon import Icon -from reflex.components.radix.themes.components.dialog import ( +from reflex_components_lucide.icon import Icon +from reflex_components_radix.themes.components.dialog import ( DialogContent, DialogRoot, DialogTitle, ) -from reflex.components.radix.themes.layout.flex import Flex -from reflex.components.radix.themes.typography.text import Text -from reflex.components.sonner.toast import ToastProps, toast_ref +from reflex_components_radix.themes.layout.flex import Flex +from reflex_components_radix.themes.typography.text import Text +from reflex_components_sonner.toast import ToastProps, toast_ref + +from reflex import constants +from reflex.components.component import Component from reflex.constants import Dirs, Hooks, Imports from reflex.constants.compiler import CompileVars from reflex.environment import environment @@ -25,6 +23,9 @@ from reflex.vars.function import FunctionStringVar from reflex.vars.number import BooleanVar from reflex.vars.sequence import LiteralArrayVar +from reflex_components_core.base.fragment import Fragment +from reflex_components_core.core.cond import cond +from reflex_components_core.el.elements.typography import Div connect_error_var_data: VarData = VarData( imports=Imports.EVENTS, diff --git a/reflex/components/core/breakpoints.py b/packages/reflex-components-core/src/reflex_components_core/core/breakpoints.py similarity index 100% rename from reflex/components/core/breakpoints.py rename to packages/reflex-components-core/src/reflex_components_core/core/breakpoints.py diff --git a/reflex/components/core/clipboard.py b/packages/reflex-components-core/src/reflex_components_core/core/clipboard.py similarity index 98% rename from reflex/components/core/clipboard.py rename to packages/reflex-components-core/src/reflex_components_core/core/clipboard.py index 0001a459f57..e60fc8572ab 100644 --- a/reflex/components/core/clipboard.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/clipboard.py @@ -4,7 +4,6 @@ from collections.abc import Sequence -from reflex.components.base.fragment import Fragment from reflex.components.component import field from reflex.components.tags.tag import Tag from reflex.constants.compiler import Hooks @@ -13,6 +12,7 @@ from reflex.utils.imports import ImportVar from reflex.vars import get_unique_variable_name from reflex.vars.base import Var, VarData +from reflex_components_core.base.fragment import Fragment class Clipboard(Fragment): diff --git a/reflex/components/core/colors.py b/packages/reflex-components-core/src/reflex_components_core/core/colors.py similarity index 100% rename from reflex/components/core/colors.py rename to packages/reflex-components-core/src/reflex_components_core/core/colors.py diff --git a/reflex/components/core/cond.py b/packages/reflex-components-core/src/reflex_components_core/core/cond.py similarity index 98% rename from reflex/components/core/cond.py rename to packages/reflex-components-core/src/reflex_components_core/core/cond.py index eefcc04ef9d..8bd258cbd9c 100644 --- a/reflex/components/core/cond.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/cond.py @@ -4,7 +4,6 @@ from typing import Any, overload -from reflex.components.base.fragment import Fragment from reflex.components.component import BaseComponent, Component, field from reflex.components.tags import CondTag, Tag from reflex.constants import Dirs @@ -14,6 +13,7 @@ from reflex.vars import VarData from reflex.vars.base import LiteralVar, Var from reflex.vars.number import ternary_operation +from reflex_components_core.base.fragment import Fragment _IS_TRUE_IMPORT: ImportDict = { f"$/{Dirs.STATE_PATH}": [ImportVar(tag="isTrue")], diff --git a/reflex/components/core/debounce.py b/packages/reflex-components-core/src/reflex_components_core/core/debounce.py similarity index 100% rename from reflex/components/core/debounce.py rename to packages/reflex-components-core/src/reflex_components_core/core/debounce.py diff --git a/reflex/components/core/foreach.py b/packages/reflex-components-core/src/reflex_components_core/core/foreach.py similarity index 98% rename from reflex/components/core/foreach.py rename to packages/reflex-components-core/src/reflex_components_core/core/foreach.py index 38f921e5cf8..731a3471475 100644 --- a/reflex/components/core/foreach.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/foreach.py @@ -8,9 +8,7 @@ from hashlib import md5 from typing import Any -from reflex.components.base.fragment import Fragment from reflex.components.component import Component, field -from reflex.components.core.cond import cond from reflex.components.tags import IterTag from reflex.constants import MemoizationMode from reflex.constants.state import FIELD_MARKER @@ -18,6 +16,8 @@ from reflex.utils import types from reflex.utils.exceptions import UntypedVarError from reflex.vars.base import LiteralVar, Var +from reflex_components_core.base.fragment import Fragment +from reflex_components_core.core.cond import cond class ForeachVarError(TypeError): diff --git a/reflex/components/core/helmet.py b/packages/reflex-components-core/src/reflex_components_core/core/helmet.py similarity index 100% rename from reflex/components/core/helmet.py rename to packages/reflex-components-core/src/reflex_components_core/core/helmet.py diff --git a/reflex/components/core/html.py b/packages/reflex-components-core/src/reflex_components_core/core/html.py similarity index 95% rename from reflex/components/core/html.py rename to packages/reflex-components-core/src/reflex_components_core/core/html.py index 33a18fbcbc4..59455f25b1b 100644 --- a/reflex/components/core/html.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/html.py @@ -1,8 +1,8 @@ """A html component.""" from reflex.components.component import field -from reflex.components.el.elements.typography import Div from reflex.vars.base import Var +from reflex_components_core.el.elements.typography import Div class Html(Div): diff --git a/reflex/components/core/layout/__init__.py b/packages/reflex-components-core/src/reflex_components_core/core/layout/__init__.py similarity index 100% rename from reflex/components/core/layout/__init__.py rename to packages/reflex-components-core/src/reflex_components_core/core/layout/__init__.py diff --git a/reflex/components/core/match.py b/packages/reflex-components-core/src/reflex_components_core/core/match.py similarity index 99% rename from reflex/components/core/match.py rename to packages/reflex-components-core/src/reflex_components_core/core/match.py index f567cb79d37..999a585006d 100644 --- a/reflex/components/core/match.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/match.py @@ -3,7 +3,6 @@ import textwrap from typing import Any, cast -from reflex.components.base import Fragment from reflex.components.component import BaseComponent, Component, MemoizationLeaf, field from reflex.components.tags import Tag from reflex.components.tags.match_tag import MatchTag @@ -13,6 +12,7 @@ from reflex.utils.imports import ImportDict from reflex.vars import VarData from reflex.vars.base import LiteralVar, Var +from reflex_components_core.base import Fragment class Match(MemoizationLeaf): diff --git a/reflex/components/core/responsive.py b/packages/reflex-components-core/src/reflex_components_core/core/responsive.py similarity index 96% rename from reflex/components/core/responsive.py rename to packages/reflex-components-core/src/reflex_components_core/core/responsive.py index e1c7f0cb305..f149ca24350 100644 --- a/reflex/components/core/responsive.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/responsive.py @@ -1,6 +1,6 @@ """Responsive components.""" -from reflex.components.radix.themes.layout.box import Box +from reflex_components_radix.themes.layout.box import Box # Add responsive styles shortcuts. diff --git a/reflex/components/core/sticky.py b/packages/reflex-components-core/src/reflex_components_core/core/sticky.py similarity index 90% rename from reflex/components/core/sticky.py rename to packages/reflex-components-core/src/reflex_components_core/core/sticky.py index 4373e0e2fc6..0758f75a6a1 100644 --- a/reflex/components/core/sticky.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/sticky.py @@ -1,13 +1,14 @@ """Components for displaying the Reflex sticky logo.""" +from reflex_components_radix.themes.typography.text import Text + from reflex.components.component import ComponentNamespace -from reflex.components.core.colors import color -from reflex.components.core.cond import color_mode_cond -from reflex.components.core.responsive import desktop_only -from reflex.components.el.elements.inline import A -from reflex.components.el.elements.media import Path, Rect, Svg -from reflex.components.radix.themes.typography.text import Text from reflex.style import Style +from reflex_components_core.core.colors import color +from reflex_components_core.core.cond import color_mode_cond +from reflex_components_core.core.responsive import desktop_only +from reflex_components_core.el.elements.inline import A +from reflex_components_core.el.elements.media import Path, Rect, Svg class StickyLogo(Svg): diff --git a/reflex/components/core/upload.py b/packages/reflex-components-core/src/reflex_components_core/core/upload.py similarity index 98% rename from reflex/components/core/upload.py rename to packages/reflex-components-core/src/reflex_components_core/core/upload.py index b6fdd476001..2beb50dd9be 100644 --- a/reflex/components/core/upload.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/upload.py @@ -6,8 +6,10 @@ from pathlib import Path from typing import Any, ClassVar +from reflex_components_radix.themes.layout.box import Box +from reflex_components_sonner.toast import toast + from reflex.app import UploadFile -from reflex.components.base.fragment import Fragment from reflex.components.component import ( Component, ComponentNamespace, @@ -15,10 +17,6 @@ StatefulComponent, field, ) -from reflex.components.core.cond import cond -from reflex.components.el.elements.forms import Input -from reflex.components.radix.themes.layout.box import Box -from reflex.components.sonner.toast import toast from reflex.constants import Dirs from reflex.constants.compiler import Hooks, Imports from reflex.environment import environment @@ -42,6 +40,9 @@ from reflex.vars.function import FunctionVar from reflex.vars.object import ObjectVar from reflex.vars.sequence import ArrayVar, LiteralStringVar +from reflex_components_core.base.fragment import Fragment +from reflex_components_core.core.cond import cond +from reflex_components_core.el.elements.forms import Input DEFAULT_UPLOAD_ID: str = "default" diff --git a/reflex/components/core/window_events.py b/packages/reflex-components-core/src/reflex_components_core/core/window_events.py similarity index 98% rename from reflex/components/core/window_events.py rename to packages/reflex-components-core/src/reflex_components_core/core/window_events.py index 3e2e980ef1f..4379ff89cc7 100644 --- a/reflex/components/core/window_events.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/window_events.py @@ -5,12 +5,12 @@ from typing import Any, cast import reflex as rx -from reflex.components.base.fragment import Fragment from reflex.components.component import StatefulComponent, field from reflex.constants.compiler import Hooks from reflex.event import key_event, no_args_event_spec from reflex.vars.base import Var, VarData from reflex.vars.object import ObjectVar +from reflex_components_core.base.fragment import Fragment def _on_resize_spec() -> tuple[Var[int], Var[int]]: diff --git a/reflex/components/datadisplay/__init__.py b/packages/reflex-components-core/src/reflex_components_core/datadisplay/__init__.py similarity index 100% rename from reflex/components/datadisplay/__init__.py rename to packages/reflex-components-core/src/reflex_components_core/datadisplay/__init__.py diff --git a/reflex/components/datadisplay/logo.py b/packages/reflex-components-core/src/reflex_components_core/datadisplay/logo.py similarity index 100% rename from reflex/components/datadisplay/logo.py rename to packages/reflex-components-core/src/reflex_components_core/datadisplay/logo.py diff --git a/reflex/components/el/__init__.py b/packages/reflex-components-core/src/reflex_components_core/el/__init__.py similarity index 92% rename from reflex/components/el/__init__.py rename to packages/reflex-components-core/src/reflex_components_core/el/__init__.py index 88acb2a313b..f0f414fe860 100644 --- a/reflex/components/el/__init__.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/__init__.py @@ -13,7 +13,7 @@ for k, attrs in elements._MAPPING.items() } _EXTRA_MAPPINGS: dict[str, str] = { - "a": "reflex.components.react_router.link", + "a": "reflex_components_react_router.link", } __getattr__, __dir__, __all__ = lazy_loader.attach( diff --git a/reflex/components/el/element.py b/packages/reflex-components-core/src/reflex_components_core/el/element.py similarity index 100% rename from reflex/components/el/element.py rename to packages/reflex-components-core/src/reflex_components_core/el/element.py diff --git a/reflex/components/el/elements/__init__.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/__init__.py similarity index 100% rename from reflex/components/el/elements/__init__.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/__init__.py diff --git a/reflex/components/el/elements/base.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/base.py similarity index 98% rename from reflex/components/el/elements/base.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/base.py index 70e9cff1beb..873972ea800 100644 --- a/reflex/components/el/elements/base.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/base.py @@ -3,8 +3,8 @@ from typing import Literal from reflex.components.component import field -from reflex.components.el.element import Element from reflex.vars.base import Var +from reflex_components_core.el.element import Element AutoCapitalize = Literal["off", "none", "on", "sentences", "words", "characters"] ContentEditable = Literal["inherit", "plaintext-only"] | bool diff --git a/reflex/components/el/elements/forms.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py similarity index 99% rename from reflex/components/el/elements/forms.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py index ce9b7de03cf..24e331e484a 100644 --- a/reflex/components/el/elements/forms.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py @@ -7,7 +7,6 @@ from typing import Any, ClassVar, Literal from reflex.components.component import field -from reflex.components.el.element import Element from reflex.components.tags.tag import Tag from reflex.constants import Dirs, EventTriggers from reflex.event import ( @@ -27,6 +26,7 @@ from reflex.vars import VarData from reflex.vars.base import LiteralVar, Var from reflex.vars.number import ternary_operation +from reflex_components_core.el.element import Element from .base import BaseHTML diff --git a/reflex/components/el/elements/inline.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/inline.py similarity index 100% rename from reflex/components/el/elements/inline.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/inline.py diff --git a/reflex/components/el/elements/media.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/media.py similarity index 99% rename from reflex/components/el/elements/media.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/media.py index a53076e124c..0e45b68e6e5 100644 --- a/reflex/components/el/elements/media.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/media.py @@ -4,9 +4,9 @@ from reflex import Component, ComponentNamespace from reflex.components.component import field -from reflex.components.el.elements.inline import ReferrerPolicy from reflex.constants.colors import Color from reflex.vars.base import Var +from reflex_components_core.el.elements.inline import ReferrerPolicy from .base import BaseHTML diff --git a/reflex/components/el/elements/metadata.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.py similarity index 92% rename from reflex/components/el/elements/metadata.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.py index 1b7c2089f1d..ebc6574fc06 100644 --- a/reflex/components/el/elements/metadata.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.py @@ -1,10 +1,10 @@ """Metadata classes.""" from reflex.components.component import field -from reflex.components.el.element import Element -from reflex.components.el.elements.inline import ReferrerPolicy -from reflex.components.el.elements.media import CrossOrigin from reflex.vars.base import Var +from reflex_components_core.el.element import Element +from reflex_components_core.el.elements.inline import ReferrerPolicy +from reflex_components_core.el.elements.media import CrossOrigin from .base import BaseHTML diff --git a/reflex/components/el/elements/other.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/other.py similarity index 100% rename from reflex/components/el/elements/other.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/other.py diff --git a/reflex/components/el/elements/scripts.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/scripts.py similarity index 90% rename from reflex/components/el/elements/scripts.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/scripts.py index 2435a749c98..0c61f9cad09 100644 --- a/reflex/components/el/elements/scripts.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/scripts.py @@ -1,9 +1,9 @@ """Scripts classes.""" from reflex.components.component import field -from reflex.components.el.elements.inline import ReferrerPolicy -from reflex.components.el.elements.media import CrossOrigin from reflex.vars.base import Var +from reflex_components_core.el.elements.inline import ReferrerPolicy +from reflex_components_core.el.elements.media import CrossOrigin from .base import BaseHTML diff --git a/reflex/components/el/elements/sectioning.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/sectioning.py similarity index 100% rename from reflex/components/el/elements/sectioning.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/sectioning.py diff --git a/reflex/components/el/elements/tables.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/tables.py similarity index 100% rename from reflex/components/el/elements/tables.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/tables.py diff --git a/reflex/components/el/elements/typography.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/typography.py similarity index 100% rename from reflex/components/el/elements/typography.py rename to packages/reflex-components-core/src/reflex_components_core/el/elements/typography.py diff --git a/packages/reflex-components-dataeditor/README.md b/packages/reflex-components-dataeditor/README.md new file mode 100644 index 00000000000..6683e72a292 --- /dev/null +++ b/packages/reflex-components-dataeditor/README.md @@ -0,0 +1,3 @@ +# reflex-components-dataeditor + +Reflex dataeditor components. diff --git a/packages/reflex-components-dataeditor/pyproject.toml b/packages/reflex-components-dataeditor/pyproject.toml new file mode 100644 index 00000000000..025f3230e3c --- /dev/null +++ b/packages/reflex-components-dataeditor/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "reflex-components-dataeditor" +dynamic = ["version"] +description = "Reflex dataeditor components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-dataeditor-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/packages/reflex-components-dataeditor/src/reflex_components_dataeditor/__init__.py b/packages/reflex-components-dataeditor/src/reflex_components_dataeditor/__init__.py new file mode 100644 index 00000000000..86d12c5542c --- /dev/null +++ b/packages/reflex-components-dataeditor/src/reflex_components_dataeditor/__init__.py @@ -0,0 +1 @@ +"""Reflex DataEditor component.""" diff --git a/reflex/components/datadisplay/dataeditor.py b/packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.py similarity index 99% rename from reflex/components/datadisplay/dataeditor.py rename to packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.py index cae0e7a9080..5b116e468d1 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.py @@ -502,7 +502,7 @@ def create(cls, *children, **props) -> Component: Raises: ValueError: invalid input. """ - from reflex.components.el import Div + from reflex_components_core.el import Div columns = props.get("columns", []) data = props.get("data", []) @@ -564,7 +564,7 @@ def _get_app_wrap_components() -> dict[tuple[int, str], Component]: Returns: The app wrap components. """ - from reflex.components.el import Div + from reflex_components_core.el import Div class Portal(Div): def get_ref(self): diff --git a/packages/reflex-components-gridjs/README.md b/packages/reflex-components-gridjs/README.md new file mode 100644 index 00000000000..f476e663587 --- /dev/null +++ b/packages/reflex-components-gridjs/README.md @@ -0,0 +1,3 @@ +# reflex-components-gridjs + +Reflex gridjs components. diff --git a/packages/reflex-components-gridjs/pyproject.toml b/packages/reflex-components-gridjs/pyproject.toml new file mode 100644 index 00000000000..1429c3de472 --- /dev/null +++ b/packages/reflex-components-gridjs/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "reflex-components-gridjs" +dynamic = ["version"] +description = "Reflex gridjs components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-gridjs-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/reflex/components/gridjs/__init__.py b/packages/reflex-components-gridjs/src/reflex_components_gridjs/__init__.py similarity index 100% rename from reflex/components/gridjs/__init__.py rename to packages/reflex-components-gridjs/src/reflex_components_gridjs/__init__.py diff --git a/reflex/components/gridjs/datatable.py b/packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.py similarity index 100% rename from reflex/components/gridjs/datatable.py rename to packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.py diff --git a/packages/reflex-components-lucide/README.md b/packages/reflex-components-lucide/README.md new file mode 100644 index 00000000000..f889e94ec50 --- /dev/null +++ b/packages/reflex-components-lucide/README.md @@ -0,0 +1,3 @@ +# reflex-components-lucide + +Reflex lucide components. diff --git a/packages/reflex-components-lucide/pyproject.toml b/packages/reflex-components-lucide/pyproject.toml new file mode 100644 index 00000000000..23c722dd8aa --- /dev/null +++ b/packages/reflex-components-lucide/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "reflex-components-lucide" +dynamic = ["version"] +description = "Reflex lucide components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-lucide-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/reflex/components/lucide/__init__.py b/packages/reflex-components-lucide/src/reflex_components_lucide/__init__.py similarity index 100% rename from reflex/components/lucide/__init__.py rename to packages/reflex-components-lucide/src/reflex_components_lucide/__init__.py diff --git a/reflex/components/lucide/icon.py b/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py similarity index 100% rename from reflex/components/lucide/icon.py rename to packages/reflex-components-lucide/src/reflex_components_lucide/icon.py diff --git a/packages/reflex-components-markdown/README.md b/packages/reflex-components-markdown/README.md new file mode 100644 index 00000000000..51648cfb68d --- /dev/null +++ b/packages/reflex-components-markdown/README.md @@ -0,0 +1,3 @@ +# reflex-components-markdown + +Reflex markdown components. diff --git a/packages/reflex-components-markdown/pyproject.toml b/packages/reflex-components-markdown/pyproject.toml new file mode 100644 index 00000000000..57743013bf5 --- /dev/null +++ b/packages/reflex-components-markdown/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "reflex-components-markdown" +dynamic = ["version"] +description = "Reflex markdown components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-markdown-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/reflex/components/markdown/__init__.py b/packages/reflex-components-markdown/src/reflex_components_markdown/__init__.py similarity index 100% rename from reflex/components/markdown/__init__.py rename to packages/reflex-components-markdown/src/reflex_components_markdown/__init__.py diff --git a/reflex/components/markdown/markdown.py b/packages/reflex-components-markdown/src/reflex_components_markdown/markdown.py similarity index 95% rename from reflex/components/markdown/markdown.py rename to packages/reflex-components-markdown/src/reflex_components_markdown/markdown.py index 8da316bb621..ae9e303f81f 100644 --- a/reflex/components/markdown/markdown.py +++ b/packages/reflex-components-markdown/src/reflex_components_markdown/markdown.py @@ -10,6 +10,8 @@ from types import SimpleNamespace from typing import Any +from reflex_components_core.el.elements.typography import Div + from reflex.components.component import ( BaseComponent, Component, @@ -17,7 +19,6 @@ CustomComponent, field, ) -from reflex.components.el.elements.typography import Div from reflex.components.tags.tag import Tag from reflex.utils import console from reflex.utils.imports import ImportDict, ImportTypes, ImportVar @@ -88,79 +89,79 @@ def create( def _h1(value: object): - from reflex.components.radix.themes.typography.heading import Heading + from reflex_components_radix.themes.typography.heading import Heading return Heading.create(value, as_="h1", size="6", margin_y="0.5em") def _h2(value: object): - from reflex.components.radix.themes.typography.heading import Heading + from reflex_components_radix.themes.typography.heading import Heading return Heading.create(value, as_="h2", size="5", margin_y="0.5em") def _h3(value: object): - from reflex.components.radix.themes.typography.heading import Heading + from reflex_components_radix.themes.typography.heading import Heading return Heading.create(value, as_="h3", size="4", margin_y="0.5em") def _h4(value: object): - from reflex.components.radix.themes.typography.heading import Heading + from reflex_components_radix.themes.typography.heading import Heading return Heading.create(value, as_="h4", size="3", margin_y="0.5em") def _h5(value: object): - from reflex.components.radix.themes.typography.heading import Heading + from reflex_components_radix.themes.typography.heading import Heading return Heading.create(value, as_="h5", size="2", margin_y="0.5em") def _h6(value: object): - from reflex.components.radix.themes.typography.heading import Heading + from reflex_components_radix.themes.typography.heading import Heading return Heading.create(value, as_="h6", size="1", margin_y="0.5em") def _p(value: object): - from reflex.components.radix.themes.typography.text import Text + from reflex_components_radix.themes.typography.text import Text return Text.create(value, margin_y="1em") def _ul(value: object): - from reflex.components.radix.themes.layout.list import UnorderedList + from reflex_components_radix.themes.layout.list import UnorderedList return UnorderedList.create(value, margin_y="1em") def _ol(value: object): - from reflex.components.radix.themes.layout.list import OrderedList + from reflex_components_radix.themes.layout.list import OrderedList return OrderedList.create(value, margin_y="1em") def _li(value: object): - from reflex.components.radix.themes.layout.list import ListItem + from reflex_components_radix.themes.layout.list import ListItem return ListItem.create(value, margin_y="0.5em") def _a(value: object): - from reflex.components.radix.themes.typography.link import Link + from reflex_components_radix.themes.typography.link import Link return Link.create(value) def _code(value: object): - from reflex.components.radix.themes.typography.code import Code + from reflex_components_radix.themes.typography.code import Code return Code.create(value) def _codeblock(value: object, **props): - from reflex.components.datadisplay.code import CodeBlock + from reflex_components_code.code import CodeBlock return CodeBlock.create(value, margin_y="1em", wrap_long_lines=True, **props) diff --git a/packages/reflex-components-moment/README.md b/packages/reflex-components-moment/README.md new file mode 100644 index 00000000000..ca7092de983 --- /dev/null +++ b/packages/reflex-components-moment/README.md @@ -0,0 +1,3 @@ +# reflex-components-moment + +Reflex moment components. diff --git a/packages/reflex-components-moment/pyproject.toml b/packages/reflex-components-moment/pyproject.toml new file mode 100644 index 00000000000..aac90ca206e --- /dev/null +++ b/packages/reflex-components-moment/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "reflex-components-moment" +dynamic = ["version"] +description = "Reflex moment components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-moment-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/reflex/components/moment/__init__.py b/packages/reflex-components-moment/src/reflex_components_moment/__init__.py similarity index 100% rename from reflex/components/moment/__init__.py rename to packages/reflex-components-moment/src/reflex_components_moment/__init__.py diff --git a/reflex/components/moment/moment.py b/packages/reflex-components-moment/src/reflex_components_moment/moment.py similarity index 100% rename from reflex/components/moment/moment.py rename to packages/reflex-components-moment/src/reflex_components_moment/moment.py diff --git a/packages/reflex-components-plotly/README.md b/packages/reflex-components-plotly/README.md new file mode 100644 index 00000000000..ebd0b1284a8 --- /dev/null +++ b/packages/reflex-components-plotly/README.md @@ -0,0 +1,3 @@ +# reflex-components-plotly + +Reflex plotly components. diff --git a/packages/reflex-components-plotly/pyproject.toml b/packages/reflex-components-plotly/pyproject.toml new file mode 100644 index 00000000000..6c9e1203c35 --- /dev/null +++ b/packages/reflex-components-plotly/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "reflex-components-plotly" +dynamic = ["version"] +description = "Reflex plotly components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-plotly-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/reflex/components/plotly/__init__.py b/packages/reflex-components-plotly/src/reflex_components_plotly/__init__.py similarity index 100% rename from reflex/components/plotly/__init__.py rename to packages/reflex-components-plotly/src/reflex_components_plotly/__init__.py diff --git a/reflex/components/plotly/plotly.py b/packages/reflex-components-plotly/src/reflex_components_plotly/plotly.py similarity index 99% rename from reflex/components/plotly/plotly.py rename to packages/reflex-components-plotly/src/reflex_components_plotly/plotly.py index 3f66a1eeffc..a4bb2dcba78 100644 --- a/reflex/components/plotly/plotly.py +++ b/packages/reflex-components-plotly/src/reflex_components_plotly/plotly.py @@ -4,8 +4,9 @@ from typing import TYPE_CHECKING, Any, TypedDict, TypeVar +from reflex_components_core.core.cond import color_mode_cond + from reflex.components.component import Component, NoSSRComponent, field -from reflex.components.core.cond import color_mode_cond from reflex.event import EventHandler, no_args_event_spec from reflex.utils import console from reflex.utils.imports import ImportDict, ImportVar diff --git a/packages/reflex-components-radix/README.md b/packages/reflex-components-radix/README.md new file mode 100644 index 00000000000..f5a060a25e3 --- /dev/null +++ b/packages/reflex-components-radix/README.md @@ -0,0 +1,3 @@ +# reflex-components-radix + +Reflex radix components. diff --git a/packages/reflex-components-radix/pyproject.toml b/packages/reflex-components-radix/pyproject.toml new file mode 100644 index 00000000000..975ca929323 --- /dev/null +++ b/packages/reflex-components-radix/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "reflex-components-radix" +dynamic = ["version"] +description = "Reflex radix components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-radix-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/reflex/components/radix/__init__.py b/packages/reflex-components-radix/src/reflex_components_radix/__init__.py similarity index 100% rename from reflex/components/radix/__init__.py rename to packages/reflex-components-radix/src/reflex_components_radix/__init__.py diff --git a/reflex/components/radix/primitives/__init__.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/__init__.py similarity index 100% rename from reflex/components/radix/primitives/__init__.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/__init__.py diff --git a/reflex/components/radix/primitives/accordion.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/accordion.py similarity index 98% rename from reflex/components/radix/primitives/accordion.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/accordion.py index 0579dffee7e..71fdc8a3f19 100644 --- a/reflex/components/radix/primitives/accordion.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/accordion.py @@ -5,17 +5,18 @@ from collections.abc import Sequence from typing import Any, ClassVar, Literal +from reflex_components_core.core.colors import color +from reflex_components_core.core.cond import cond +from reflex_components_lucide.icon import Icon + from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.colors import color -from reflex.components.core.cond import cond -from reflex.components.lucide.icon import Icon -from reflex.components.radix.primitives.base import RadixPrimitiveComponent -from reflex.components.radix.themes.base import LiteralAccentColor, LiteralRadius from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler from reflex.style import Style from reflex.vars import get_uuid_string_var from reflex.vars.base import LiteralVar, Var +from reflex_components_radix.primitives.base import RadixPrimitiveComponent +from reflex_components_radix.themes.base import LiteralAccentColor, LiteralRadius LiteralAccordionType = Literal["single", "multiple"] LiteralAccordionDir = Literal["ltr", "rtl"] diff --git a/reflex/components/radix/primitives/base.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/base.py similarity index 96% rename from reflex/components/radix/primitives/base.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/base.py index 21a6815e557..6fe5112d884 100644 --- a/reflex/components/radix/primitives/base.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/base.py @@ -49,7 +49,7 @@ def create(cls, *children: Any, **props: Any) -> Component: Returns: The new RadixPrimitiveTriggerComponent instance. """ - from reflex.components.el.elements.typography import Div + from reflex_components_core.el.elements.typography import Div for child in children: if "on_click" in getattr(child, "event_triggers", {}): diff --git a/reflex/components/radix/primitives/dialog.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/dialog.py similarity index 99% rename from reflex/components/radix/primitives/dialog.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/dialog.py index c559346ea0a..987ef6f3a61 100644 --- a/reflex/components/radix/primitives/dialog.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/dialog.py @@ -2,8 +2,9 @@ from typing import Any, ClassVar +from reflex_components_core.el import elements + from reflex.components.component import ComponentNamespace, field -from reflex.components.el import elements from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var diff --git a/reflex/components/radix/primitives/drawer.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/drawer.py similarity index 98% rename from reflex/components/radix/primitives/drawer.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/drawer.py index 35a7950ab53..a2ceb2b79de 100644 --- a/reflex/components/radix/primitives/drawer.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/drawer.py @@ -8,12 +8,12 @@ from typing import Any, Literal from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.radix.primitives.base import RadixPrimitiveComponent -from reflex.components.radix.themes.base import Theme -from reflex.components.radix.themes.layout.flex import Flex from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var +from reflex_components_radix.primitives.base import RadixPrimitiveComponent +from reflex_components_radix.themes.base import Theme +from reflex_components_radix.themes.layout.flex import Flex class DrawerComponent(RadixPrimitiveComponent): diff --git a/reflex/components/radix/primitives/form.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/form.py similarity index 96% rename from reflex/components/radix/primitives/form.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/form.py index 3ef2bb8dc30..73d5f4d78ed 100644 --- a/reflex/components/radix/primitives/form.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/form.py @@ -4,12 +4,13 @@ from typing import Any, Literal +from reflex_components_core.core.debounce import DebounceInput +from reflex_components_core.el.elements.forms import Form as HTMLForm + from reflex.components.component import ComponentNamespace, field -from reflex.components.core.debounce import DebounceInput -from reflex.components.el.elements.forms import Form as HTMLForm -from reflex.components.radix.themes.components.text_field import TextFieldRoot from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import Var +from reflex_components_radix.themes.components.text_field import TextFieldRoot from .base import RadixPrimitiveComponentWithClassName diff --git a/reflex/components/radix/primitives/progress.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/progress.py similarity index 94% rename from reflex/components/radix/primitives/progress.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/progress.py index 6ad5d367bfb..8dea7cdf9f7 100644 --- a/reflex/components/radix/primitives/progress.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/progress.py @@ -4,12 +4,13 @@ from typing import Any +from reflex_components_core.core.colors import color + from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.colors import color -from reflex.components.radix.primitives.accordion import DEFAULT_ANIMATION_DURATION -from reflex.components.radix.primitives.base import RadixPrimitiveComponentWithClassName -from reflex.components.radix.themes.base import LiteralAccentColor, LiteralRadius from reflex.vars.base import Var +from reflex_components_radix.primitives.accordion import DEFAULT_ANIMATION_DURATION +from reflex_components_radix.primitives.base import RadixPrimitiveComponentWithClassName +from reflex_components_radix.themes.base import LiteralAccentColor, LiteralRadius class ProgressComponent(RadixPrimitiveComponentWithClassName): diff --git a/reflex/components/radix/primitives/slider.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/slider.py similarity index 98% rename from reflex/components/radix/primitives/slider.py rename to packages/reflex-components-radix/src/reflex_components_radix/primitives/slider.py index b04b5f6bb74..3d132cd05b0 100644 --- a/reflex/components/radix/primitives/slider.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/slider.py @@ -6,9 +6,9 @@ from typing import Any, Literal from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.radix.primitives.base import RadixPrimitiveComponentWithClassName from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var +from reflex_components_radix.primitives.base import RadixPrimitiveComponentWithClassName LiteralSliderOrientation = Literal["horizontal", "vertical"] LiteralSliderDir = Literal["ltr", "rtl"] diff --git a/reflex/components/radix/themes/__init__.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/__init__.py similarity index 100% rename from reflex/components/radix/themes/__init__.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/__init__.py diff --git a/reflex/components/radix/themes/base.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/base.py similarity index 99% rename from reflex/components/radix/themes/base.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/base.py index 22cbc82c3fa..aa0a39c9d1c 100644 --- a/reflex/components/radix/themes/base.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/base.py @@ -4,9 +4,10 @@ from typing import Any, ClassVar, Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components import Component from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive from reflex.components.tags import Tag from reflex.utils.imports import ImportDict, ImportVar from reflex.vars.base import Var diff --git a/reflex/components/radix/themes/color_mode.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/color_mode.py similarity index 96% rename from reflex/components/radix/themes/color_mode.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/color_mode.py index a444df79c5d..4b5bccef52c 100644 --- a/reflex/components/radix/themes/color_mode.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/color_mode.py @@ -19,11 +19,10 @@ from typing import Any, Literal, get_args +from reflex_components_core.core.cond import Cond, color_mode_cond, cond +from reflex_components_lucide.icon import Icon + from reflex.components.component import BaseComponent, field -from reflex.components.core.cond import Cond, color_mode_cond, cond -from reflex.components.lucide.icon import Icon -from reflex.components.radix.themes.components.dropdown_menu import dropdown_menu -from reflex.components.radix.themes.components.switch import Switch from reflex.style import ( LIGHT_COLOR_MODE, color_mode, @@ -33,6 +32,8 @@ ) from reflex.vars.base import Var from reflex.vars.sequence import LiteralArrayVar +from reflex_components_radix.themes.components.dropdown_menu import dropdown_menu +from reflex_components_radix.themes.components.switch import Switch from .components.icon_button import IconButton diff --git a/reflex/components/radix/themes/components/__init__.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/__init__.py similarity index 100% rename from reflex/components/radix/themes/components/__init__.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/__init__.py diff --git a/reflex/components/radix/themes/components/alert_dialog.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/alert_dialog.py similarity index 95% rename from reflex/components/radix/themes/components/alert_dialog.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/alert_dialog.py index 17c38511b6c..500924d51a8 100644 --- a/reflex/components/radix/themes/components/alert_dialog.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/alert_dialog.py @@ -2,16 +2,17 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( - RadixThemesComponent, - RadixThemesTriggerComponent, -) from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( + RadixThemesComponent, + RadixThemesTriggerComponent, +) LiteralContentSize = Literal["1", "2", "3", "4"] diff --git a/reflex/components/radix/themes/components/aspect_ratio.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/aspect_ratio.py similarity index 86% rename from reflex/components/radix/themes/components/aspect_ratio.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/aspect_ratio.py index d71811b6dd2..805fb0ed194 100644 --- a/reflex/components/radix/themes/components/aspect_ratio.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/aspect_ratio.py @@ -1,8 +1,8 @@ """Interactive components provided by @radix-ui/themes.""" from reflex.components.component import field -from reflex.components.radix.themes.base import RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import RadixThemesComponent class AspectRatio(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/avatar.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/avatar.py similarity index 90% rename from reflex/components/radix/themes/components/avatar.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/avatar.py index 2f22e7498e0..13fc8f27242 100644 --- a/reflex/components/radix/themes/components/avatar.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/avatar.py @@ -2,14 +2,15 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import ( +from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralRadius, RadixThemesComponent, ) -from reflex.vars.base import Var LiteralSize = Literal["1", "2", "3", "4", "5", "6", "7", "8", "9"] diff --git a/reflex/components/radix/themes/components/badge.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/badge.py similarity index 85% rename from reflex/components/radix/themes/components/badge.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/badge.py index a9f51dba97b..66dbd6bb9ad 100644 --- a/reflex/components/radix/themes/components/badge.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/badge.py @@ -2,15 +2,16 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralRadius, RadixThemesComponent, ) -from reflex.vars.base import Var class Badge(elements.Span, RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/button.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/button.py similarity index 88% rename from reflex/components/radix/themes/components/button.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/button.py index 6caa4362514..94032f3b480 100644 --- a/reflex/components/radix/themes/components/button.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/button.py @@ -2,17 +2,18 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralRadius, LiteralVariant, RadixLoadingProp, RadixThemesComponent, ) -from reflex.vars.base import Var LiteralButtonSize = Literal["1", "2", "3", "4"] diff --git a/reflex/components/radix/themes/components/callout.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/callout.py similarity index 91% rename from reflex/components/radix/themes/components/callout.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/callout.py index b51f40e6bdc..4b01e051400 100644 --- a/reflex/components/radix/themes/components/callout.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/callout.py @@ -2,13 +2,14 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_components_lucide.icon import Icon + import reflex as rx from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.lucide.icon import Icon -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent CalloutVariant = Literal["soft", "surface", "outline"] diff --git a/reflex/components/radix/themes/components/card.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/card.py similarity index 80% rename from reflex/components/radix/themes/components/card.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/card.py index 10234dbeb67..fa0655cc583 100644 --- a/reflex/components/radix/themes/components/card.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/card.py @@ -2,11 +2,12 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import RadixThemesComponent class Card(elements.Div, RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/checkbox.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox.py similarity index 95% rename from reflex/components/radix/themes/components/checkbox.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox.py index d9b4dee7d28..6da370ac347 100644 --- a/reflex/components/radix/themes/components/checkbox.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox.py @@ -2,17 +2,18 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import ( +from reflex.event import EventHandler, passthrough_event_spec +from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralSpacing, RadixThemesComponent, ) -from reflex.components.radix.themes.layout.flex import Flex -from reflex.components.radix.themes.typography.text import Text -from reflex.event import EventHandler, passthrough_event_spec -from reflex.vars.base import Var +from reflex_components_radix.themes.layout.flex import Flex +from reflex_components_radix.themes.typography.text import Text LiteralCheckboxSize = Literal["1", "2", "3"] LiteralCheckboxVariant = Literal["classic", "surface", "soft"] diff --git a/reflex/components/radix/themes/components/checkbox_cards.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_cards.py similarity index 91% rename from reflex/components/radix/themes/components/checkbox_cards.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_cards.py index e8bf228bb72..a7eccd91248 100644 --- a/reflex/components/radix/themes/components/checkbox_cards.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_cards.py @@ -3,10 +3,11 @@ from types import SimpleNamespace from typing import Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent class CheckboxCardsRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/checkbox_group.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_group.py similarity index 92% rename from reflex/components/radix/themes/components/checkbox_group.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_group.py index e3146157e71..388d6b48afd 100644 --- a/reflex/components/radix/themes/components/checkbox_group.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_group.py @@ -4,10 +4,11 @@ from types import SimpleNamespace from typing import Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent class CheckboxGroupRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/context_menu.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/context_menu.py similarity index 99% rename from reflex/components/radix/themes/components/context_menu.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/context_menu.py index 0d53b4d173d..b7ad94ff799 100644 --- a/reflex/components/radix/themes/components/context_menu.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/context_menu.py @@ -2,12 +2,13 @@ from typing import ClassVar, Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent from .checkbox import Checkbox from .radio_group import HighLevelRadioGroup diff --git a/reflex/components/radix/themes/components/data_list.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/data_list.py similarity index 93% rename from reflex/components/radix/themes/components/data_list.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/data_list.py index b8217eda3e1..a052bbcaaf1 100644 --- a/reflex/components/radix/themes/components/data_list.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/data_list.py @@ -3,10 +3,11 @@ from types import SimpleNamespace from typing import Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent class DataListRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/dialog.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/dialog.py similarity index 94% rename from reflex/components/radix/themes/components/dialog.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/dialog.py index dfbd3cae21f..bc7ab03bed2 100644 --- a/reflex/components/radix/themes/components/dialog.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/dialog.py @@ -2,16 +2,17 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( - RadixThemesComponent, - RadixThemesTriggerComponent, -) from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( + RadixThemesComponent, + RadixThemesTriggerComponent, +) class DialogRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/dropdown_menu.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/dropdown_menu.py similarity index 99% rename from reflex/components/radix/themes/components/dropdown_menu.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/dropdown_menu.py index dd074e8539f..b3817993dfa 100644 --- a/reflex/components/radix/themes/components/dropdown_menu.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/dropdown_menu.py @@ -2,16 +2,17 @@ from typing import ClassVar, Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import ( +from reflex.constants.compiler import MemoizationMode +from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec +from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( LiteralAccentColor, RadixThemesComponent, RadixThemesTriggerComponent, ) -from reflex.constants.compiler import MemoizationMode -from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec -from reflex.vars.base import Var LiteralDirType = Literal["ltr", "rtl"] diff --git a/reflex/components/radix/themes/components/hover_card.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/hover_card.py similarity index 95% rename from reflex/components/radix/themes/components/hover_card.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/hover_card.py index 5a779c19b53..71c81063949 100644 --- a/reflex/components/radix/themes/components/hover_card.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/hover_card.py @@ -2,16 +2,17 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( - RadixThemesComponent, - RadixThemesTriggerComponent, -) from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( + RadixThemesComponent, + RadixThemesTriggerComponent, +) class HoverCardRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/icon_button.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/icon_button.py similarity index 92% rename from reflex/components/radix/themes/components/icon_button.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/icon_button.py index 002d0740312..82f313c8012 100644 --- a/reflex/components/radix/themes/components/icon_button.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/icon_button.py @@ -4,20 +4,21 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.match import Match +from reflex_components_core.el import elements +from reflex_components_lucide import Icon + from reflex.components.component import Component, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.core.match import Match -from reflex.components.el import elements -from reflex.components.lucide import Icon -from reflex.components.radix.themes.base import ( +from reflex.style import Style +from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralRadius, LiteralVariant, RadixLoadingProp, RadixThemesComponent, ) -from reflex.style import Style -from reflex.vars.base import Var LiteralButtonSize = Literal["1", "2", "3", "4"] diff --git a/reflex/components/radix/themes/components/inset.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/inset.py similarity index 87% rename from reflex/components/radix/themes/components/inset.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/inset.py index 8d4a45a79e4..90abd565187 100644 --- a/reflex/components/radix/themes/components/inset.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/inset.py @@ -2,11 +2,12 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import RadixThemesComponent LiteralButtonSize = Literal["1", "2", "3", "4"] diff --git a/reflex/components/radix/themes/components/popover.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/popover.py similarity index 96% rename from reflex/components/radix/themes/components/popover.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/popover.py index 9c9f248b7cd..e61916d523b 100644 --- a/reflex/components/radix/themes/components/popover.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/popover.py @@ -2,16 +2,17 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( - RadixThemesComponent, - RadixThemesTriggerComponent, -) from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( + RadixThemesComponent, + RadixThemesTriggerComponent, +) class PopoverRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/progress.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/progress.py similarity index 95% rename from reflex/components/radix/themes/components/progress.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/progress.py index 97b0701325a..4993a85bea1 100644 --- a/reflex/components/radix/themes/components/progress.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/progress.py @@ -2,11 +2,12 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import Component, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.style import Style from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent class Progress(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/radio.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio.py similarity index 87% rename from reflex/components/radix/themes/components/radio.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio.py index 87493f758d6..5e92cc6041f 100644 --- a/reflex/components/radix/themes/components/radio.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio.py @@ -2,10 +2,11 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent class Radio(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/radio_cards.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_cards.py similarity index 96% rename from reflex/components/radix/themes/components/radio_cards.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_cards.py index 674446b5dff..e1e9078d42b 100644 --- a/reflex/components/radix/themes/components/radio_cards.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_cards.py @@ -3,11 +3,12 @@ from types import SimpleNamespace from typing import ClassVar, Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent class RadioCardsRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/radio_group.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_group.py similarity index 96% rename from reflex/components/radix/themes/components/radio_group.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_group.py index 32167edbad3..4137e813878 100644 --- a/reflex/components/radix/themes/components/radio_group.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_group.py @@ -5,20 +5,21 @@ from collections.abc import Sequence from typing import Literal +from reflex_components_core.core.breakpoints import Responsive + import reflex as rx from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import ( - LiteralAccentColor, - LiteralSpacing, - RadixThemesComponent, -) -from reflex.components.radix.themes.layout.flex import Flex -from reflex.components.radix.themes.typography.text import Text from reflex.event import EventHandler, passthrough_event_spec from reflex.utils import types from reflex.vars.base import LiteralVar, Var from reflex.vars.sequence import StringVar +from reflex_components_radix.themes.base import ( + LiteralAccentColor, + LiteralSpacing, + RadixThemesComponent, +) +from reflex_components_radix.themes.layout.flex import Flex +from reflex_components_radix.themes.typography.text import Text LiteralFlexDirection = Literal["row", "column", "row-reverse", "column-reverse"] diff --git a/reflex/components/radix/themes/components/scroll_area.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/scroll_area.py similarity index 93% rename from reflex/components/radix/themes/components/scroll_area.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/scroll_area.py index ca1b6a2d1e0..afde09818bb 100644 --- a/reflex/components/radix/themes/components/scroll_area.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/scroll_area.py @@ -3,8 +3,8 @@ from typing import Literal from reflex.components.component import field -from reflex.components.radix.themes.base import RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import RadixThemesComponent class ScrollArea(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/segmented_control.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/segmented_control.py similarity index 94% rename from reflex/components/radix/themes/components/segmented_control.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/segmented_control.py index b8cadb1b512..0d32b1484dd 100644 --- a/reflex/components/radix/themes/components/segmented_control.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/segmented_control.py @@ -6,11 +6,12 @@ from types import SimpleNamespace from typing import ClassVar, Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.event import EventHandler from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent def on_value_change( diff --git a/reflex/components/radix/themes/components/select.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/select.py similarity index 98% rename from reflex/components/radix/themes/components/select.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/select.py index c4592224e80..69a1188520a 100644 --- a/reflex/components/radix/themes/components/select.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/select.py @@ -3,17 +3,18 @@ from collections.abc import Sequence from typing import ClassVar, Literal +from reflex_components_core.core.breakpoints import Responsive + import reflex as rx from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import ( +from reflex.constants.compiler import MemoizationMode +from reflex.event import no_args_event_spec, passthrough_event_spec +from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralRadius, RadixThemesComponent, ) -from reflex.constants.compiler import MemoizationMode -from reflex.event import no_args_event_spec, passthrough_event_spec -from reflex.vars.base import Var class SelectRoot(RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/separator.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/separator.py similarity index 88% rename from reflex/components/radix/themes/components/separator.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/separator.py index fd0d4f9fa2b..3d9db2a21a1 100644 --- a/reflex/components/radix/themes/components/separator.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/separator.py @@ -2,10 +2,11 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.vars.base import LiteralVar, Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent LiteralSeparatorSize = Literal["1", "2", "3", "4"] diff --git a/reflex/components/radix/themes/components/skeleton.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/skeleton.py similarity index 87% rename from reflex/components/radix/themes/components/skeleton.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/skeleton.py index 4e33ec64a12..fea674943dc 100644 --- a/reflex/components/radix/themes/components/skeleton.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/skeleton.py @@ -1,10 +1,11 @@ """Skeleton theme from Radix components.""" +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import RadixLoadingProp, RadixThemesComponent from reflex.constants.compiler import MemoizationMode from reflex.vars.base import Var +from reflex_components_radix.themes.base import RadixLoadingProp, RadixThemesComponent class Skeleton(RadixLoadingProp, RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/slider.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/slider.py similarity index 96% rename from reflex/components/radix/themes/components/slider.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/slider.py index 04836bcdea7..ff0e6499486 100644 --- a/reflex/components/radix/themes/components/slider.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/slider.py @@ -5,12 +5,13 @@ from collections.abc import Sequence from typing import Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import Component, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.event import EventHandler, passthrough_event_spec from reflex.utils.types import typehint_issubclass from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent on_value_event_spec = ( passthrough_event_spec(list[float]), diff --git a/reflex/components/radix/themes/components/spinner.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/spinner.py similarity index 78% rename from reflex/components/radix/themes/components/spinner.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/spinner.py index 57d9a497645..d9eccfb0fee 100644 --- a/reflex/components/radix/themes/components/spinner.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/spinner.py @@ -2,10 +2,11 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import RadixLoadingProp, RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import RadixLoadingProp, RadixThemesComponent LiteralSpinnerSize = Literal["1", "2", "3"] diff --git a/reflex/components/radix/themes/components/switch.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/switch.py similarity index 93% rename from reflex/components/radix/themes/components/switch.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/switch.py index 6c465bfbecd..eee57dac486 100644 --- a/reflex/components/radix/themes/components/switch.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/switch.py @@ -2,11 +2,12 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent LiteralSwitchSize = Literal["1", "2", "3"] diff --git a/reflex/components/radix/themes/components/table.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/table.py similarity index 95% rename from reflex/components/radix/themes/components/table.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/table.py index 13d92d27848..f352e4a8820 100644 --- a/reflex/components/radix/themes/components/table.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/table.py @@ -2,11 +2,12 @@ from typing import ClassVar, Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import CommonPaddingProps, RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import CommonPaddingProps, RadixThemesComponent class TableRoot(elements.Table, RadixThemesComponent): diff --git a/reflex/components/radix/themes/components/tabs.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/tabs.py similarity index 96% rename from reflex/components/radix/themes/components/tabs.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/tabs.py index eb5c136cd71..e6d2eb51feb 100644 --- a/reflex/components/radix/themes/components/tabs.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/tabs.py @@ -4,13 +4,14 @@ from typing import Any, ClassVar, Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.colors import color + from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.core.colors import color -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent vertical_orientation_css = "&[data-orientation='vertical']" diff --git a/reflex/components/radix/themes/components/text_area.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_area.py similarity index 94% rename from reflex/components/radix/themes/components/text_area.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_area.py index e76dc01be9e..60b9f3e104f 100644 --- a/reflex/components/radix/themes/components/text_area.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_area.py @@ -2,16 +2,17 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.debounce import DebounceInput +from reflex_components_core.el import elements + from reflex.components.component import Component, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.core.debounce import DebounceInput -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralRadius, RadixThemesComponent, ) -from reflex.vars.base import Var LiteralTextAreaSize = Literal["1", "2", "3"] diff --git a/reflex/components/radix/themes/components/text_field.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_field.py similarity index 95% rename from reflex/components/radix/themes/components/text_field.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_field.py index 8e8d581c36b..24ff5d7612a 100644 --- a/reflex/components/radix/themes/components/text_field.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_field.py @@ -4,19 +4,20 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.debounce import DebounceInput +from reflex_components_core.el import elements + from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.core.debounce import DebounceInput -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( - LiteralAccentColor, - LiteralRadius, - RadixThemesComponent, -) from reflex.event import EventHandler, input_event, key_event from reflex.utils.types import is_optional from reflex.vars.base import Var from reflex.vars.number import ternary_operation +from reflex_components_radix.themes.base import ( + LiteralAccentColor, + LiteralRadius, + RadixThemesComponent, +) LiteralTextFieldSize = Literal["1", "2", "3"] LiteralTextFieldVariant = Literal["classic", "surface", "soft"] diff --git a/reflex/components/radix/themes/components/tooltip.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/tooltip.py similarity index 98% rename from reflex/components/radix/themes/components/tooltip.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/components/tooltip.py index 9915f7a5c72..90c9b2a8a8a 100644 --- a/reflex/components/radix/themes/components/tooltip.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/components/tooltip.py @@ -3,11 +3,11 @@ from typing import Literal from reflex.components.component import Component, field -from reflex.components.radix.themes.base import RadixThemesComponent from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.utils import format from reflex.vars.base import Var +from reflex_components_radix.themes.base import RadixThemesComponent LiteralSideType = Literal[ "top", diff --git a/reflex/components/radix/themes/layout/__init__.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/__init__.py similarity index 100% rename from reflex/components/radix/themes/layout/__init__.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/__init__.py diff --git a/reflex/components/radix/themes/layout/base.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/base.py similarity index 88% rename from reflex/components/radix/themes/layout/base.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/base.py index b7f512ea6e2..f90ad69d63e 100644 --- a/reflex/components/radix/themes/layout/base.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/base.py @@ -4,14 +4,15 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import ( +from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( CommonMarginProps, CommonPaddingProps, RadixThemesComponent, ) -from reflex.vars.base import Var LiteralBoolNumber = Literal["0", "1"] diff --git a/reflex/components/radix/themes/layout/box.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/box.py similarity index 68% rename from reflex/components/radix/themes/layout/box.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/box.py index 3932d2b9e1a..16d8d0fbf33 100644 --- a/reflex/components/radix/themes/layout/box.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/box.py @@ -2,8 +2,9 @@ from __future__ import annotations -from reflex.components.el import elements -from reflex.components.radix.themes.base import RadixThemesComponent +from reflex_components_core.el import elements + +from reflex_components_radix.themes.base import RadixThemesComponent class Box(elements.Div, RadixThemesComponent): diff --git a/reflex/components/radix/themes/layout/center.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/center.py similarity index 100% rename from reflex/components/radix/themes/layout/center.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/center.py diff --git a/reflex/components/radix/themes/layout/container.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/container.py similarity index 89% rename from reflex/components/radix/themes/layout/container.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/container.py index e44b583d2c9..9cdcc9080cf 100644 --- a/reflex/components/radix/themes/layout/container.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/container.py @@ -4,12 +4,13 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import RadixThemesComponent from reflex.style import STACK_CHILDREN_FULL_WIDTH from reflex.vars.base import LiteralVar, Var +from reflex_components_radix.themes.base import RadixThemesComponent LiteralContainerSize = Literal["1", "2", "3", "4"] diff --git a/reflex/components/radix/themes/layout/flex.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/flex.py similarity index 90% rename from reflex/components/radix/themes/layout/flex.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/flex.py index 80101d27e47..9b59dc4766a 100644 --- a/reflex/components/radix/themes/layout/flex.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/flex.py @@ -4,16 +4,17 @@ from typing import ClassVar, Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( LiteralAlign, LiteralJustify, LiteralSpacing, RadixThemesComponent, ) -from reflex.vars.base import Var LiteralFlexDirection = Literal["row", "column", "row-reverse", "column-reverse"] LiteralFlexWrap = Literal["nowrap", "wrap", "wrap-reverse"] diff --git a/reflex/components/radix/themes/layout/grid.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/grid.py similarity index 91% rename from reflex/components/radix/themes/layout/grid.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/grid.py index 186fa3c11a7..5e9298f8c5d 100644 --- a/reflex/components/radix/themes/layout/grid.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/grid.py @@ -4,16 +4,17 @@ from typing import ClassVar, Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import ( +from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( LiteralAlign, LiteralJustify, LiteralSpacing, RadixThemesComponent, ) -from reflex.vars.base import Var LiteralGridFlow = Literal["row", "column", "dense", "row-dense", "column-dense"] diff --git a/reflex/components/radix/themes/layout/list.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/list.py similarity index 93% rename from reflex/components/radix/themes/layout/list.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/list.py index b39a56b2e0d..a1ac0cec6f0 100644 --- a/reflex/components/radix/themes/layout/list.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/list.py @@ -5,14 +5,15 @@ from collections.abc import Iterable from typing import Any, Literal +from reflex_components_core.core.foreach import Foreach +from reflex_components_core.el.elements.base import BaseHTML +from reflex_components_core.el.elements.typography import Li, Ol, Ul +from reflex_components_lucide.icon import Icon +from reflex_components_markdown.markdown import MarkdownComponentMap + from reflex.components.component import ComponentNamespace, field -from reflex.components.core.foreach import Foreach -from reflex.components.el.elements.base import BaseHTML -from reflex.components.el.elements.typography import Li, Ol, Ul -from reflex.components.lucide.icon import Icon -from reflex.components.markdown.markdown import MarkdownComponentMap -from reflex.components.radix.themes.typography.text import Text from reflex.vars.base import Var +from reflex_components_radix.themes.typography.text import Text LiteralListStyleTypeUnordered = Literal[ "none", diff --git a/reflex/components/radix/themes/layout/section.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/section.py similarity index 76% rename from reflex/components/radix/themes/layout/section.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/section.py index 88fe24cbad9..b7ec569abbc 100644 --- a/reflex/components/radix/themes/layout/section.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/section.py @@ -4,11 +4,12 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import RadixThemesComponent from reflex.vars.base import LiteralVar, Var +from reflex_components_radix.themes.base import RadixThemesComponent LiteralSectionSize = Literal["1", "2", "3"] diff --git a/reflex/components/radix/themes/layout/spacer.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/spacer.py similarity index 100% rename from reflex/components/radix/themes/layout/spacer.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/spacer.py diff --git a/reflex/components/radix/themes/layout/stack.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/stack.py similarity index 92% rename from reflex/components/radix/themes/layout/stack.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/layout/stack.py index 98f9c2f9170..c7463875fb8 100644 --- a/reflex/components/radix/themes/layout/stack.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/layout/stack.py @@ -2,10 +2,11 @@ from __future__ import annotations +from reflex_components_core.core.breakpoints import Responsive + from reflex.components.component import Component, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.radix.themes.base import LiteralAlign, LiteralSpacing from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAlign, LiteralSpacing from .flex import Flex, LiteralFlexDirection diff --git a/reflex/components/radix/themes/typography/__init__.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/__init__.py similarity index 100% rename from reflex/components/radix/themes/typography/__init__.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/__init__.py diff --git a/reflex/components/radix/themes/typography/base.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/base.py similarity index 100% rename from reflex/components/radix/themes/typography/base.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/base.py diff --git a/reflex/components/radix/themes/typography/blockquote.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/blockquote.py similarity index 83% rename from reflex/components/radix/themes/typography/blockquote.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/blockquote.py index 836b8286bd7..689af36f89e 100644 --- a/reflex/components/radix/themes/typography/blockquote.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/blockquote.py @@ -5,11 +5,12 @@ from __future__ import annotations +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent from .base import LiteralTextSize, LiteralTextWeight diff --git a/reflex/components/radix/themes/typography/code.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/code.py similarity index 82% rename from reflex/components/radix/themes/typography/code.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/code.py index ade8c441d1e..a4c4c7248a4 100644 --- a/reflex/components/radix/themes/typography/code.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/code.py @@ -5,16 +5,17 @@ from __future__ import annotations +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_components_markdown.markdown import MarkdownComponentMap + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.markdown.markdown import MarkdownComponentMap -from reflex.components.radix.themes.base import ( +from reflex.vars.base import Var +from reflex_components_radix.themes.base import ( LiteralAccentColor, LiteralVariant, RadixThemesComponent, ) -from reflex.vars.base import Var from .base import LiteralTextSize, LiteralTextWeight diff --git a/reflex/components/radix/themes/typography/heading.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/heading.py similarity index 86% rename from reflex/components/radix/themes/typography/heading.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/heading.py index d970ad15ad8..e0799976ff9 100644 --- a/reflex/components/radix/themes/typography/heading.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/heading.py @@ -5,12 +5,13 @@ from __future__ import annotations +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_components_markdown.markdown import MarkdownComponentMap + from reflex.components.component import field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.markdown.markdown import MarkdownComponentMap -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent from .base import LiteralTextAlign, LiteralTextSize, LiteralTextTrim, LiteralTextWeight diff --git a/reflex/components/radix/themes/typography/link.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/link.py similarity index 89% rename from reflex/components/radix/themes/typography/link.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/link.py index 1e20f36942f..bf6fbe6dd7c 100644 --- a/reflex/components/radix/themes/typography/link.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/link.py @@ -7,16 +7,17 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.core.colors import color +from reflex_components_core.core.cond import cond +from reflex_components_core.el.elements.inline import A +from reflex_components_markdown.markdown import MarkdownComponentMap +from reflex_components_react_router.dom import ReactRouterLink + from reflex.components.component import Component, MemoizationLeaf, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.core.colors import color -from reflex.components.core.cond import cond -from reflex.components.el.elements.inline import A -from reflex.components.markdown.markdown import MarkdownComponentMap -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent -from reflex.components.react_router.dom import ReactRouterLink from reflex.utils.imports import ImportDict, ImportVar from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent from .base import LiteralTextSize, LiteralTextTrim, LiteralTextWeight diff --git a/reflex/components/radix/themes/typography/text.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/text.py similarity index 92% rename from reflex/components/radix/themes/typography/text.py rename to packages/reflex-components-radix/src/reflex_components_radix/themes/typography/text.py index 1c90bd83a32..7a79546bd34 100644 --- a/reflex/components/radix/themes/typography/text.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/typography/text.py @@ -7,12 +7,13 @@ from typing import Literal +from reflex_components_core.core.breakpoints import Responsive +from reflex_components_core.el import elements +from reflex_components_markdown.markdown import MarkdownComponentMap + from reflex.components.component import ComponentNamespace, field -from reflex.components.core.breakpoints import Responsive -from reflex.components.el import elements -from reflex.components.markdown.markdown import MarkdownComponentMap -from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent from reflex.vars.base import Var +from reflex_components_radix.themes.base import LiteralAccentColor, RadixThemesComponent from .base import LiteralTextAlign, LiteralTextSize, LiteralTextTrim, LiteralTextWeight diff --git a/packages/reflex-components-react-player/README.md b/packages/reflex-components-react-player/README.md new file mode 100644 index 00000000000..b1570fb70f5 --- /dev/null +++ b/packages/reflex-components-react-player/README.md @@ -0,0 +1,3 @@ +# reflex-components-react-player + +Reflex react-player components. diff --git a/packages/reflex-components-react-player/pyproject.toml b/packages/reflex-components-react-player/pyproject.toml new file mode 100644 index 00000000000..f142c057526 --- /dev/null +++ b/packages/reflex-components-react-player/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "reflex-components-react-player" +dynamic = ["version"] +description = "Reflex react-player components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-react-player-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/reflex/components/react_player/__init__.py b/packages/reflex-components-react-player/src/reflex_components_react_player/__init__.py similarity index 100% rename from reflex/components/react_player/__init__.py rename to packages/reflex-components-react-player/src/reflex_components_react_player/__init__.py diff --git a/reflex/components/react_player/audio.py b/packages/reflex-components-react-player/src/reflex_components_react_player/audio.py similarity index 63% rename from reflex/components/react_player/audio.py rename to packages/reflex-components-react-player/src/reflex_components_react_player/audio.py index 2f5cc5b6d8d..2580fa26944 100644 --- a/reflex/components/react_player/audio.py +++ b/packages/reflex-components-react-player/src/reflex_components_react_player/audio.py @@ -1,6 +1,6 @@ """A audio component.""" -from reflex.components.react_player.react_player import ReactPlayer +from reflex_components_react_player.react_player import ReactPlayer class Audio(ReactPlayer): diff --git a/reflex/components/react_player/react_player.py b/packages/reflex-components-react-player/src/reflex_components_react_player/react_player.py similarity index 99% rename from reflex/components/react_player/react_player.py rename to packages/reflex-components-react-player/src/reflex_components_react_player/react_player.py index f75779a322a..af01d475c88 100644 --- a/reflex/components/react_player/react_player.py +++ b/packages/reflex-components-react-player/src/reflex_components_react_player/react_player.py @@ -4,8 +4,9 @@ from typing import Any, TypedDict +from reflex_components_core.core.cond import cond + from reflex.components.component import Component, field -from reflex.components.core.cond import cond from reflex.event import EventHandler, no_args_event_spec from reflex.utils import console from reflex.vars.base import Var diff --git a/reflex/components/react_player/video.py b/packages/reflex-components-react-player/src/reflex_components_react_player/video.py similarity index 63% rename from reflex/components/react_player/video.py rename to packages/reflex-components-react-player/src/reflex_components_react_player/video.py index 70b513195e2..df0dba45502 100644 --- a/reflex/components/react_player/video.py +++ b/packages/reflex-components-react-player/src/reflex_components_react_player/video.py @@ -1,6 +1,6 @@ """A video component.""" -from reflex.components.react_player.react_player import ReactPlayer +from reflex_components_react_player.react_player import ReactPlayer class Video(ReactPlayer): diff --git a/packages/reflex-components-react-router/README.md b/packages/reflex-components-react-router/README.md new file mode 100644 index 00000000000..f4465ac2200 --- /dev/null +++ b/packages/reflex-components-react-router/README.md @@ -0,0 +1,3 @@ +# reflex-components-react-router + +Reflex react-router components. diff --git a/packages/reflex-components-react-router/pyproject.toml b/packages/reflex-components-react-router/pyproject.toml new file mode 100644 index 00000000000..cdf35b0305e --- /dev/null +++ b/packages/reflex-components-react-router/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "reflex-components-react-router" +dynamic = ["version"] +description = "Reflex react-router components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-react-router-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/reflex/components/react_router/__init__.py b/packages/reflex-components-react-router/src/reflex_components_react_router/__init__.py similarity index 100% rename from reflex/components/react_router/__init__.py rename to packages/reflex-components-react-router/src/reflex_components_react_router/__init__.py diff --git a/reflex/components/react_router/dom.py b/packages/reflex-components-react-router/src/reflex_components_react_router/dom.py similarity index 97% rename from reflex/components/react_router/dom.py rename to packages/reflex-components-react-router/src/reflex_components_react_router/dom.py index 5654c829a89..9f744773ee8 100644 --- a/reflex/components/react_router/dom.py +++ b/packages/reflex-components-react-router/src/reflex_components_react_router/dom.py @@ -4,8 +4,9 @@ from typing import ClassVar, Literal, TypedDict +from reflex_components_core.el.elements.inline import A + from reflex.components.component import field -from reflex.components.el.elements.inline import A from reflex.vars.base import Var LiteralLinkDiscover = Literal["none", "render"] diff --git a/packages/reflex-components-recharts/README.md b/packages/reflex-components-recharts/README.md new file mode 100644 index 00000000000..f5e6de0e026 --- /dev/null +++ b/packages/reflex-components-recharts/README.md @@ -0,0 +1,3 @@ +# reflex-components-recharts + +Reflex recharts components. diff --git a/packages/reflex-components-recharts/pyproject.toml b/packages/reflex-components-recharts/pyproject.toml new file mode 100644 index 00000000000..8367651473e --- /dev/null +++ b/packages/reflex-components-recharts/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "reflex-components-recharts" +dynamic = ["version"] +description = "Reflex recharts components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-recharts-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/reflex/components/recharts/__init__.py b/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py similarity index 100% rename from reflex/components/recharts/__init__.py rename to packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py diff --git a/reflex/components/recharts/cartesian.py b/packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py similarity index 100% rename from reflex/components/recharts/cartesian.py rename to packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.py diff --git a/reflex/components/recharts/charts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py similarity index 99% rename from reflex/components/recharts/charts.py rename to packages/reflex-components-recharts/src/reflex_components_recharts/charts.py index 5a62c4d6dce..c9a9853c269 100644 --- a/reflex/components/recharts/charts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py @@ -6,11 +6,11 @@ from typing import Any, ClassVar from reflex.components.component import Component, field -from reflex.components.recharts.general import ResponsiveContainer from reflex.constants import EventTriggers from reflex.constants.colors import Color from reflex.event import EventHandler, no_args_event_spec from reflex.vars.base import Var +from reflex_components_recharts.general import ResponsiveContainer from .recharts import ( LiteralAnimationEasing, diff --git a/reflex/components/recharts/general.py b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py similarity index 100% rename from reflex/components/recharts/general.py rename to packages/reflex-components-recharts/src/reflex_components_recharts/general.py diff --git a/reflex/components/recharts/polar.py b/packages/reflex-components-recharts/src/reflex_components_recharts/polar.py similarity index 100% rename from reflex/components/recharts/polar.py rename to packages/reflex-components-recharts/src/reflex_components_recharts/polar.py diff --git a/reflex/components/recharts/recharts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py similarity index 100% rename from reflex/components/recharts/recharts.py rename to packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py diff --git a/packages/reflex-components-sonner/README.md b/packages/reflex-components-sonner/README.md new file mode 100644 index 00000000000..cfc4e39ac49 --- /dev/null +++ b/packages/reflex-components-sonner/README.md @@ -0,0 +1,3 @@ +# reflex-components-sonner + +Reflex sonner components. diff --git a/packages/reflex-components-sonner/pyproject.toml b/packages/reflex-components-sonner/pyproject.toml new file mode 100644 index 00000000000..9cb844413d9 --- /dev/null +++ b/packages/reflex-components-sonner/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "reflex-components-sonner" +dynamic = ["version"] +description = "Reflex sonner components." +readme = "README.md" +authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] +requires-python = ">=3.10" +dependencies = [] + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-components-sonner-" +fallback-version = "0.0.0dev0" + +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/reflex/components/sonner/__init__.py b/packages/reflex-components-sonner/src/reflex_components_sonner/__init__.py similarity index 100% rename from reflex/components/sonner/__init__.py rename to packages/reflex-components-sonner/src/reflex_components_sonner/__init__.py diff --git a/reflex/components/sonner/toast.py b/packages/reflex-components-sonner/src/reflex_components_sonner/toast.py similarity index 99% rename from reflex/components/sonner/toast.py rename to packages/reflex-components-sonner/src/reflex_components_sonner/toast.py index 330a24478c5..5514d1a8c80 100644 --- a/reflex/components/sonner/toast.py +++ b/packages/reflex-components-sonner/src/reflex_components_sonner/toast.py @@ -5,8 +5,9 @@ import dataclasses from typing import Any, Literal +from reflex_components_lucide.icon import Icon + from reflex.components.component import Component, ComponentNamespace, field -from reflex.components.lucide.icon import Icon from reflex.components.props import NoExtrasAllowedProps from reflex.constants.base import Dirs from reflex.event import EventSpec, run_script diff --git a/packages/reflex-docgen/README.md b/packages/reflex-docgen/README.md index e69de29bb2d..40d2d955dcb 100644 --- a/packages/reflex-docgen/README.md +++ b/packages/reflex-docgen/README.md @@ -0,0 +1,3 @@ +# reflex-docgen + +Generate documentation for Reflex components. diff --git a/packages/reflex-docgen/pyproject.toml b/packages/reflex-docgen/pyproject.toml index 21d9d7c39c1..fcd2e920d7e 100644 --- a/packages/reflex-docgen/pyproject.toml +++ b/packages/reflex-docgen/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "reflex-docgen" -version = "0.0.1" +dynamic = ["version"] description = "Generate documentation for Reflex components." readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] @@ -8,6 +8,8 @@ maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] requires-python = ">=3.10" dependencies = [ "griffelib>=2.0.1", + "mistletoe>=1.4.0", + "pyyaml>=6.0", "reflex", "typing-extensions", "typing-inspection>=0.4.2", @@ -16,6 +18,13 @@ dependencies = [ [tool.uv.sources] reflex = { workspace = true } +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +pattern-prefix = "reflex-docgen-" +fallback-version = "0.0.0dev0" + [build-system] -requires = ["uv_build>=0.10.11,<0.11.0"] -build-backend = "uv_build" +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" diff --git a/packages/reflex-docgen/src/reflex_docgen/__init__.py b/packages/reflex-docgen/src/reflex_docgen/__init__.py index acf408b8aa9..bd22e88d501 100644 --- a/packages/reflex-docgen/src/reflex_docgen/__init__.py +++ b/packages/reflex-docgen/src/reflex_docgen/__init__.py @@ -1,5 +1,6 @@ """Module for generating documentation for Reflex components and classes.""" +from reflex_docgen import markdown as markdown from reflex_docgen._class import ClassDocumentation as ClassDocumentation from reflex_docgen._class import FieldDocumentation as FieldDocumentation from reflex_docgen._class import MethodDocumentation as MethodDocumentation @@ -28,4 +29,5 @@ "generate_documentation", "get_component_event_handlers", "get_component_props", + "markdown", ] diff --git a/packages/reflex-docgen/src/reflex_docgen/markdown/__init__.py b/packages/reflex-docgen/src/reflex_docgen/markdown/__init__.py new file mode 100644 index 00000000000..3b8ca760cc9 --- /dev/null +++ b/packages/reflex-docgen/src/reflex_docgen/markdown/__init__.py @@ -0,0 +1,55 @@ +"""Markdown parsing and types for Reflex documentation.""" + +from reflex_docgen.markdown._parser import parse_document as parse_document +from reflex_docgen.markdown._types import Block as Block +from reflex_docgen.markdown._types import BoldSpan as BoldSpan +from reflex_docgen.markdown._types import CodeBlock as CodeBlock +from reflex_docgen.markdown._types import CodeSpan as CodeSpan +from reflex_docgen.markdown._types import ComponentPreview as ComponentPreview +from reflex_docgen.markdown._types import DirectiveBlock as DirectiveBlock +from reflex_docgen.markdown._types import Document as Document +from reflex_docgen.markdown._types import FrontMatter as FrontMatter +from reflex_docgen.markdown._types import HeadingBlock as HeadingBlock +from reflex_docgen.markdown._types import ImageSpan as ImageSpan +from reflex_docgen.markdown._types import ItalicSpan as ItalicSpan +from reflex_docgen.markdown._types import LineBreakSpan as LineBreakSpan +from reflex_docgen.markdown._types import LinkSpan as LinkSpan +from reflex_docgen.markdown._types import ListBlock as ListBlock +from reflex_docgen.markdown._types import ListItem as ListItem +from reflex_docgen.markdown._types import QuoteBlock as QuoteBlock +from reflex_docgen.markdown._types import Span as Span +from reflex_docgen.markdown._types import StrikethroughSpan as StrikethroughSpan +from reflex_docgen.markdown._types import TableBlock as TableBlock +from reflex_docgen.markdown._types import TableCell as TableCell +from reflex_docgen.markdown._types import TableRow as TableRow +from reflex_docgen.markdown._types import TextBlock as TextBlock +from reflex_docgen.markdown._types import TextSpan as TextSpan +from reflex_docgen.markdown._types import ThematicBreakBlock as ThematicBreakBlock + +__all__ = [ + "Block", + "BoldSpan", + "CodeBlock", + "CodeSpan", + "ComponentPreview", + "DirectiveBlock", + "Document", + "FrontMatter", + "HeadingBlock", + "ImageSpan", + "ItalicSpan", + "LineBreakSpan", + "LinkSpan", + "ListBlock", + "ListItem", + "QuoteBlock", + "Span", + "StrikethroughSpan", + "TableBlock", + "TableCell", + "TableRow", + "TextBlock", + "TextSpan", + "ThematicBreakBlock", + "parse_document", +] diff --git a/packages/reflex-docgen/src/reflex_docgen/markdown/_parser.py b/packages/reflex-docgen/src/reflex_docgen/markdown/_parser.py new file mode 100644 index 00000000000..a5c8346b2f9 --- /dev/null +++ b/packages/reflex-docgen/src/reflex_docgen/markdown/_parser.py @@ -0,0 +1,378 @@ +"""Parse Reflex documentation markdown files using mistletoe.""" + +from __future__ import annotations + +import re +from collections.abc import Sequence +from typing import TYPE_CHECKING, cast + +from reflex_docgen.markdown._types import ( + Block, + BoldSpan, + CodeBlock, + CodeSpan, + ComponentPreview, + DirectiveBlock, + Document, + FrontMatter, + HeadingBlock, + ImageSpan, + ItalicSpan, + LineBreakSpan, + LinkSpan, + ListBlock, + ListItem, + QuoteBlock, + Span, + StrikethroughSpan, + TableBlock, + TableCell, + TableRow, + TextBlock, + TextSpan, + ThematicBreakBlock, +) + +if TYPE_CHECKING: + from mistletoe.block_token import BlockToken + +_FRONTMATTER_RE = re.compile(r"\A---\n(.*?\n)---\n", re.DOTALL) + + +#: Known frontmatter keys that are not component preview lambdas. +_KNOWN_KEYS = frozenset({"components", "only_low_level", "title"}) + + +def _extract_frontmatter(source: str) -> tuple[FrontMatter | None, str]: + """Extract YAML frontmatter from the beginning of a markdown string. + + Args: + source: The raw markdown source. + + Returns: + A tuple of (FrontMatter or None, remaining source). + """ + m = _FRONTMATTER_RE.match(source) + if m is None: + return None, source + + import yaml + + data = yaml.safe_load(m.group(1)) + if not isinstance(data, dict): + data = {} + + # components + raw_components = data.get("components", []) + if not isinstance(raw_components, list): + raw_components = [] + components = tuple(str(c) for c in raw_components) + + # only_low_level + raw_oll = data.get("only_low_level", []) + if isinstance(raw_oll, list): + only_low_level = bool(raw_oll and raw_oll[0]) + else: + only_low_level = bool(raw_oll) + + # title + raw_title = data.get("title") + title = str(raw_title) if raw_title is not None else None + + # component previews — any key not in _KNOWN_KEYS with a string value + previews: list[ComponentPreview] = [] + for key, value in data.items(): + if key not in _KNOWN_KEYS and isinstance(value, str): + previews.append(ComponentPreview(name=key, source=value.strip())) + + return ( + FrontMatter( + components=components, + only_low_level=only_low_level, + title=title, + component_previews=tuple(previews), + ), + source[m.end() :], + ) + + +def _convert_span(token: object) -> Span: + """Convert a mistletoe span token into a Span. + + Args: + token: A mistletoe span token. + + Returns: + The corresponding Span. + """ + from mistletoe.span_token import ( + Emphasis, + EscapeSequence, + Image, + InlineCode, + LineBreak, + Link, + RawText, + Strikethrough, + Strong, + ) + + if isinstance(token, RawText): + return TextSpan(text=token.content) + + if isinstance(token, InlineCode): + # InlineCode.children is a tuple of one RawText element. + children = token.children + if ( + not isinstance(children, tuple) + or len(children) != 1 + or not isinstance(children[0], RawText) + ): + msg = ( + f"Expected InlineCode to have a single RawText child, got {children!r}" + ) + raise TypeError(msg) + return CodeSpan(code=children[0].content) + + if isinstance(token, Strong): + return BoldSpan(children=_convert_children(token)) + + if isinstance(token, Emphasis): + return ItalicSpan(children=_convert_children(token)) + + if isinstance(token, Strikethrough): + return StrikethroughSpan(children=_convert_children(token)) + + if isinstance(token, Link): + return LinkSpan(children=_convert_children(token), target=token.target) + + if isinstance(token, Image): + return ImageSpan(children=_convert_children(token), src=token.src) + + if isinstance(token, LineBreak): + return LineBreakSpan(soft=token.soft) + + if isinstance(token, EscapeSequence): + # EscapeSequence.children is a tuple of one RawText with the escaped char. + children = token.children + if ( + not isinstance(children, tuple) + or len(children) != 1 + or not isinstance(children[0], RawText) + ): + msg = f"Expected EscapeSequence to have a single RawText child, got {children!r}" + raise TypeError(msg) + return TextSpan(text=children[0].content) + + msg = f"Unsupported span token type: {type(token).__name__}" + raise TypeError(msg) + + +def _convert_children(token: object) -> tuple[Span, ...]: + """Convert the children of a mistletoe token into Spans. + + Args: + token: A mistletoe token with a children attribute. + + Returns: + A tuple of Span objects. + """ + children = getattr(token, "children", None) + if children is None: + return () + return tuple(_convert_span(child) for child in children) + + +def _parse_info_string(info: str) -> tuple[str | None, tuple[str, ...]]: + """Parse a fenced code block info string into language and flags. + + Args: + info: The info string (e.g. "python demo exec"). + + Returns: + A tuple of (language, flags). + """ + parts = info.strip().split() + if not parts: + return None, () + return parts[0], tuple(parts[1:]) + + +def _convert_block(token: BlockToken) -> Block | None: + """Convert a mistletoe block token into a docgen Block. + + Args: + token: The mistletoe block token. + + Returns: + A Block, or None if the token should be skipped. + """ + from mistletoe.block_token import BlockCode as MistletoeBlockCode + from mistletoe.block_token import CodeFence as MistletoeCodeFence + from mistletoe.block_token import Heading as MistletoeHeading + from mistletoe.block_token import List as MistletoeList + from mistletoe.block_token import ListItem as MistletoeListItem + from mistletoe.block_token import Paragraph as MistletoeParagraph + from mistletoe.block_token import Quote as MistletoeQuote + from mistletoe.block_token import SetextHeading as MistletoeSetextHeading + from mistletoe.block_token import Table as MistletoeTable + from mistletoe.block_token import ThematicBreak as MistletoeThematicBreak + from mistletoe.span_token import RawText as MistletoeRawText + + if isinstance(token, (MistletoeHeading, MistletoeSetextHeading)): + return HeadingBlock(level=token.level, children=_convert_children(token)) + + if isinstance(token, (MistletoeBlockCode, MistletoeCodeFence)): + if isinstance(token, MistletoeCodeFence): + # CodeFence.info_string contains the full info string including language. + info = token.info_string or "" + else: + info = getattr(token, "language", "") or "" + language, flags = _parse_info_string(info) + + # CodeFence/BlockCode.children is a tuple of one RawText element. + children = token.children + if not children: + content = "" + elif ( + not isinstance(children, tuple) + or len(children) != 1 + or not isinstance(children[0], MistletoeRawText) + ): + msg = ( + f"Expected code block to have a single RawText child, got {children!r}" + ) + raise TypeError(msg) + else: + content = children[0].content + # Strip trailing newline that mistletoe adds. + content = content.rstrip("\n") + + # ```md [args...]``` blocks become DirectiveBlocks. + if language == "md" and flags: + return DirectiveBlock( + name=flags[0], + args=flags[1:], + content=content, + ) + + return CodeBlock(language=language, flags=flags, content=content) + + if isinstance(token, MistletoeParagraph): + spans = _convert_children(token) + if spans: + return TextBlock(children=spans) + return None + + if isinstance(token, MistletoeList): + items: list[ListItem] = [] + if token.children: + for item_token in token.children: + if not isinstance(item_token, MistletoeListItem): + msg = f"Expected ListItem, got {type(item_token).__name__}" + raise TypeError(msg) + item_blocks = _convert_block_children(item_token) + items.append(ListItem(children=item_blocks)) + # List.start is an instance attribute (int | None) but pyright sees + # the classmethod start(cls, line) instead. + list_start = cast("int | None", token.start) # pyright: ignore[reportAttributeAccessIssue] + return ListBlock( + ordered=list_start is not None, + start=list_start, + items=tuple(items), + ) + + if isinstance(token, MistletoeQuote): + return QuoteBlock(children=_convert_block_children(token)) + + if isinstance(token, MistletoeTable): + header = _convert_table_row(token.header, token.column_align) + rows = [ + _convert_table_row(row_token, token.column_align) + for row_token in (token.children or ()) + ] + return TableBlock(header=header, rows=tuple(rows)) + + if isinstance(token, MistletoeThematicBreak): + return ThematicBreakBlock() + + msg = f"Unsupported block token type: {type(token).__name__}" + raise TypeError(msg) + + +def _convert_block_children(token: BlockToken) -> tuple[Block, ...]: + """Convert the block-level children of a container token. + + Args: + token: A mistletoe container block token. + + Returns: + A tuple of Block objects. + """ + from mistletoe.block_token import BlockToken + + if not token.children: + return () + result: list[Block] = [] + for child in token.children: + if not isinstance(child, BlockToken): + msg = f"Expected BlockToken, got {type(child).__name__}" + raise TypeError(msg) + block = _convert_block(child) + if block is not None: + result.append(block) + return tuple(result) + + +def _convert_table_row( + row_token: object, column_align: Sequence[int | None] +) -> TableRow: + """Convert a mistletoe TableRow into a TableRow. + + Args: + row_token: A mistletoe TableRow token. + column_align: The column alignment list from the Table. + + Returns: + A TableRow. + """ + align_map = {None: None, 0: None, 1: "left", 2: "right", 3: "center"} + children = getattr(row_token, "children", None) or () + cells = [ + TableCell( + children=_convert_children(cell_token), + align=align_map.get(column_align[i] if i < len(column_align) else None), + ) + for i, cell_token in enumerate(children) + ] + return TableRow(cells=tuple(cells)) + + +def parse_document(source: str) -> Document: + """Parse a Reflex documentation markdown file into a Document. + + Args: + source: The raw markdown source string. + + Returns: + A parsed Document with frontmatter and content blocks. + """ + from mistletoe.block_token import BlockToken + from mistletoe.block_token import Document as MistletoeDocument + + frontmatter, remaining = _extract_frontmatter(source) + doc = MistletoeDocument(remaining) + + blocks: list[Block] = [] + if doc.children: + for child in doc.children: + # mistletoe.block_token.Document guarantees children are BlockToken + # instances (see its docstring: "Its children are block tokens"). + if not isinstance(child, BlockToken): + msg = f"Expected BlockToken, got {type(child).__name__}" + raise TypeError(msg) + block = _convert_block(child) + if block is not None: + blocks.append(block) + + return Document(frontmatter=frontmatter, blocks=tuple(blocks)) diff --git a/packages/reflex-docgen/src/reflex_docgen/markdown/_types.py b/packages/reflex-docgen/src/reflex_docgen/markdown/_types.py new file mode 100644 index 00000000000..152689718a8 --- /dev/null +++ b/packages/reflex-docgen/src/reflex_docgen/markdown/_types.py @@ -0,0 +1,558 @@ +"""Markdown document types — spans, blocks, and document.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass + +# --------------------------------------------------------------------------- +# Span types — inline content without exposing mistletoe +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TextSpan: + """Plain text. + + Attributes: + text: The text content. + """ + + text: str + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + return self.text + + +@dataclass(frozen=True, slots=True, kw_only=True) +class BoldSpan: + """Bold (strong) text. + + Attributes: + children: The inline spans inside the bold. + """ + + children: tuple[Span, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + inner = "".join(c.as_markdown() for c in self.children) + return f"**{inner}**" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ItalicSpan: + """Italic (emphasis) text. + + Attributes: + children: The inline spans inside the italic. + """ + + children: tuple[Span, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + inner = "".join(c.as_markdown() for c in self.children) + return f"*{inner}*" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class StrikethroughSpan: + """Strikethrough text. + + Attributes: + children: The inline spans inside the strikethrough. + """ + + children: tuple[Span, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + inner = "".join(c.as_markdown() for c in self.children) + return f"~~{inner}~~" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class CodeSpan: + """Inline code. + + Attributes: + code: The code text. + """ + + code: str + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + return f"`{self.code}`" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class LinkSpan: + """A hyperlink. + + Attributes: + children: The inline spans forming the link text. + target: The URL target. + """ + + children: tuple[Span, ...] + target: str + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + inner = "".join(c.as_markdown() for c in self.children) + return f"[{inner}]({self.target})" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ImageSpan: + """An inline image. + + Attributes: + children: The inline spans forming the alt text. + src: The image source URL. + """ + + children: tuple[Span, ...] + src: str + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + inner = "".join(c.as_markdown() for c in self.children) + return f"![{inner}]({self.src})" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class LineBreakSpan: + """A line break. + + Attributes: + soft: Whether this is a soft line break. + """ + + soft: bool + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + return "\n" if self.soft else " \n" + + +#: Union of all inline span types. +Span = ( + TextSpan + | BoldSpan + | ItalicSpan + | StrikethroughSpan + | CodeSpan + | LinkSpan + | ImageSpan + | LineBreakSpan +) + + +def _spans_as_markdown(spans: tuple[Span, ...]) -> str: + """Render a sequence of spans back to markdown. + + Args: + spans: The inline spans to render. + + Returns: + A markdown string. + """ + return "".join(s.as_markdown() for s in spans) + + +_BACKTICK_FENCE_RE = re.compile(r"^(`{3,})", re.MULTILINE) + + +def _fence_for(content: str) -> str: + """Return a backtick fence long enough to wrap *content* safely. + + Args: + content: The code block content that may contain backtick fences. + + Returns: + A backtick fence string (at least 3 backticks). + """ + max_run = 3 + for m in _BACKTICK_FENCE_RE.finditer(content): + max_run = max(max_run, len(m.group(1)) + 1) + return "`" * max_run + + +# --------------------------------------------------------------------------- +# Block types +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ComponentPreview: + """A component preview lambda from frontmatter. + + Attributes: + name: The component class name (e.g. "Button", "DialogRoot"). + source: The lambda source string. + """ + + name: str + source: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class FrontMatter: + """YAML frontmatter extracted from a markdown document. + + Attributes: + components: Component paths to document (e.g. ``["rx.button"]``). + only_low_level: Whether to show only low-level component variants. + title: An optional page title. + component_previews: Preview lambdas keyed by component class name. + """ + + components: tuple[str, ...] + only_low_level: bool + title: str | None + component_previews: tuple[ComponentPreview, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + import yaml + + data: dict[str, object] = {} + if self.components: + data["components"] = list(self.components) + if self.only_low_level: + data["only_low_level"] = [True] + if self.title is not None: + data["title"] = self.title + for preview in self.component_previews: + data[preview.name] = preview.source + return f"---\n{yaml.dump(data, default_flow_style=False, sort_keys=False).rstrip()}\n---" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class CodeBlock: + """A fenced code block with optional language and flags. + + Attributes: + language: The language identifier (e.g. "python"). + flags: Additional flags after the language (e.g. ("demo", "exec")). + content: The code content inside the block. + """ + + language: str | None + flags: tuple[str, ...] + content: str + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + info = self.language or "" + if self.flags: + info = f"{info} {' '.join(self.flags)}" if info else " ".join(self.flags) + fence = _fence_for(self.content) + return f"{fence}{info}\n{self.content}\n{fence}" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DirectiveBlock: + """A markdown directive block (```md [args...] ```). + + Covers alert, video, definition, section, and any future md directives. + + Attributes: + name: The directive name (e.g. "alert", "video", "definition", "section"). + args: Additional arguments after the name (e.g. ("info",) or ("https://...",)). + content: The raw content inside the block. + """ + + name: str + args: tuple[str, ...] + content: str + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + info_parts = ["md", self.name, *self.args] + fence = _fence_for(self.content) + return f"{fence}{' '.join(info_parts)}\n{self.content}\n{fence}" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class HeadingBlock: + """A markdown heading. + + Attributes: + level: The heading level (1-6). + children: The inline spans forming the heading text. + """ + + level: int + children: tuple[Span, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + return f"{'#' * self.level} {_spans_as_markdown(self.children)}" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TextBlock: + """A block of markdown text (paragraph or other inline content). + + Attributes: + children: The inline spans forming the text content. + """ + + children: tuple[Span, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + return _spans_as_markdown(self.children) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ListItem: + """A single item in a list. + + Attributes: + children: The block-level content of the list item. + """ + + children: tuple[Block, ...] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ListBlock: + """An ordered or unordered list. + + Attributes: + ordered: Whether the list is ordered. + start: The starting number for ordered lists, or None for unordered. + items: The list items. + """ + + ordered: bool + start: int | None + items: tuple[ListItem, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + lines: list[str] = [] + for i, item in enumerate(self.items): + prefix = f"{(self.start or 1) + i}. " if self.ordered else "- " + item_md = "\n\n".join(child.as_markdown() for child in item.children) + first, *rest = item_md.split("\n") + lines.append(f"{prefix}{first}") + indent = " " * len(prefix) + lines.extend(f"{indent}{line}" if line else "" for line in rest) + return "\n".join(lines) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class QuoteBlock: + """A block quote. + + Attributes: + children: The block-level content inside the quote. + """ + + children: tuple[Block, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + inner = "\n\n".join(child.as_markdown() for child in self.children) + return "\n".join(f"> {line}" if line else ">" for line in inner.split("\n")) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TableCell: + """A single cell in a table. + + Attributes: + children: The inline spans forming the cell content. + align: The column alignment ("left", "right", "center", or None). + """ + + children: tuple[Span, ...] + align: str | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TableRow: + """A row in a table. + + Attributes: + cells: The cells in the row. + """ + + cells: tuple[TableCell, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + cells = " | ".join(_spans_as_markdown(cell.children) for cell in self.cells) + return f"| {cells} |" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TableBlock: + """A table. + + Attributes: + header: The header row. + rows: The body rows. + """ + + header: TableRow + rows: tuple[TableRow, ...] + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + lines = [self.header.as_markdown()] + sep_parts: list[str] = [] + for cell in self.header.cells: + if cell.align == "left": + sep_parts.append(":---") + elif cell.align == "right": + sep_parts.append("---:") + elif cell.align == "center": + sep_parts.append(":---:") + else: + sep_parts.append("---") + lines.append(f"| {' | '.join(sep_parts)} |") + lines.extend(row.as_markdown() for row in self.rows) + return "\n".join(lines) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ThematicBreakBlock: + """A thematic break (horizontal rule).""" + + def as_markdown(self) -> str: + """Render back to markdown. + + Returns: + A markdown string. + """ + return "---" + + +#: Union of all block types that can appear in a parsed document. +Block = ( + FrontMatter + | CodeBlock + | DirectiveBlock + | HeadingBlock + | TextBlock + | ListBlock + | QuoteBlock + | TableBlock + | ThematicBreakBlock +) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Document: + """A parsed Reflex documentation markdown file. + + Attributes: + frontmatter: The YAML frontmatter, if present. + blocks: The sequence of content blocks in document order. + """ + + frontmatter: FrontMatter | None + blocks: tuple[Block, ...] + + @property + def headings(self) -> tuple[HeadingBlock, ...]: + """Return all headings in the document.""" + return tuple(b for b in self.blocks if isinstance(b, HeadingBlock)) + + @property + def code_blocks(self) -> tuple[CodeBlock, ...]: + """Return all code blocks in the document.""" + return tuple(b for b in self.blocks if isinstance(b, CodeBlock)) + + @property + def directives(self) -> tuple[DirectiveBlock, ...]: + """Return all directive blocks in the document.""" + return tuple(b for b in self.blocks if isinstance(b, DirectiveBlock)) + + def as_markdown(self) -> str: + """Render the full document back to markdown. + + Returns: + A markdown string. + """ + parts: list[str] = [] + if self.frontmatter: + parts.append(self.frontmatter.as_markdown()) + parts.extend(block.as_markdown() for block in self.blocks) + return "\n\n".join(parts) + "\n" diff --git a/pyi_hashes.json b/pyi_hashes.json index 03d20e2342b..d3bf4838e6c 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -1,122 +1,123 @@ { - "reflex/__init__.pyi": "0a3ae880e256b9fd3b960e12a2cb51a7", - "reflex/components/__init__.pyi": "ac05995852baa81062ba3d18fbc489fb", - "reflex/components/base/__init__.pyi": "16e47bf19e0d62835a605baa3d039c5a", - "reflex/components/base/app_wrap.pyi": "22e94feaa9fe675bcae51c412f5b67f1", - "reflex/components/base/body.pyi": "fa8e4343880c7b3ac09b056bd3c253ee", - "reflex/components/base/document.pyi": "1c90cb8f1981a0f7f6087c702feb23a3", - "reflex/components/base/error_boundary.pyi": "53deea0de4b36a6e0af7067e3c05e695", - "reflex/components/base/fragment.pyi": "d3da5a5bff969ea682a86eaa6658b7ec", - "reflex/components/base/link.pyi": "7ffdf9c70724da5781284641f3ce8370", - "reflex/components/base/meta.pyi": "ceb1b79d42e7b3e115e1c3ca6f7121c2", - "reflex/components/base/script.pyi": "c136448d69727928443f8ced053c7210", - "reflex/components/base/strict_mode.pyi": "e7e7358393ff81e9283e2e67f7c2aaa8", - "reflex/components/core/__init__.pyi": "007170b97e58bdf28b2aee381d91c0c7", - "reflex/components/core/auto_scroll.pyi": "a4db8095e145926992a8272379d27257", - "reflex/components/core/banner.pyi": "66f4fd0cd78a9d6071caa31c51394a49", - "reflex/components/core/clipboard.pyi": "c8a7834ea5f6202c760065b2534ea59d", - "reflex/components/core/debounce.pyi": "1722c092d6e17406a9bf047353d05ea6", - "reflex/components/core/helmet.pyi": "cb5ac1be02c6f82fcc78ba74651be593", - "reflex/components/core/html.pyi": "4ebe946f3fc097fc2e31dddf7040ec1c", - "reflex/components/core/sticky.pyi": "cb763b986a9b0654d1a3f33440dfcf60", - "reflex/components/core/upload.pyi": "c90782be1b63276b428bce3fd4ce0af2", - "reflex/components/core/window_events.pyi": "e7af4bf5341c4afaf60c4a534660f68f", - "reflex/components/datadisplay/__init__.pyi": "52755871369acbfd3a96b46b9a11d32e", - "reflex/components/datadisplay/code.pyi": "1d123d19ef08f085422f3023540e7bb1", - "reflex/components/datadisplay/dataeditor.pyi": "93309b17a4c12b2216e2d863d325a107", - "reflex/components/datadisplay/shiki_code_block.pyi": "570c1a03ad509da982b90de42c69fd47", - "reflex/components/el/__init__.pyi": "0adfd001a926a2a40aee94f6fa725ecc", - "reflex/components/el/element.pyi": "62431eed73f5a2b0536036ce05fb84bd", - "reflex/components/el/elements/__init__.pyi": "29512d7a6b29c6dc5ff68d3b31f26528", - "reflex/components/el/elements/base.pyi": "705e5555b86b2fff64cc77baf6ed02f1", - "reflex/components/el/elements/forms.pyi": "5a1fde0f8fee4d1bec938577781d6e53", - "reflex/components/el/elements/inline.pyi": "c7340e5c60344c61aa7a1c30b3e1b92f", - "reflex/components/el/elements/media.pyi": "fa92cf81b560466b310ad9f5795e9fcf", - "reflex/components/el/elements/metadata.pyi": "67ac22ad50139f545bc0e2e26fe31c04", - "reflex/components/el/elements/other.pyi": "b4bc739f1338acd430263b5bc284b6ab", - "reflex/components/el/elements/scripts.pyi": "00731f16568572131dcc3e02d2a7aa66", - "reflex/components/el/elements/sectioning.pyi": "c8bc889c30bcb54a270fb07ff1d2c7cc", - "reflex/components/el/elements/tables.pyi": "7c76a00f319dbbc27ea1a4d81695e4be", - "reflex/components/el/elements/typography.pyi": "b6a5b6a0a23a03cd81a96ddb4769076a", - "reflex/components/gridjs/datatable.pyi": "761a2472cef297005f8d7ce63a06ee27", - "reflex/components/lucide/icon.pyi": "1f0449a8dc8ea7016334f4d51a42ce1a", - "reflex/components/markdown/markdown.pyi": "9e7316a36a36409d718700609652e570", - "reflex/components/moment/moment.pyi": "b63ea2a7e91f4caf8db86e438caead5a", - "reflex/components/plotly/plotly.pyi": "af31e1963b6788f3dec3238310269b7c", - "reflex/components/radix/__init__.pyi": "5d8e3579912473e563676bfc71f29191", - "reflex/components/radix/primitives/__init__.pyi": "01c388fe7a1f5426a16676404344edf6", - "reflex/components/radix/primitives/accordion.pyi": "a0b2f56c5a726057596b77b336879bd9", - "reflex/components/radix/primitives/base.pyi": "e64ef8c92e94e02baa2193c6b3da8300", - "reflex/components/radix/primitives/dialog.pyi": "2995a7323d8a016db9945955db2d2070", - "reflex/components/radix/primitives/drawer.pyi": "0041ba408b1171d6a733335d5dfb2f5d", - "reflex/components/radix/primitives/form.pyi": "7eb90bcdb45a9de263eea722733d7704", - "reflex/components/radix/primitives/progress.pyi": "959e0540affc4967c457da7f6642b7ae", - "reflex/components/radix/primitives/slider.pyi": "3892b9e10cd424a4ea40f166737eeaf8", - "reflex/components/radix/themes/__init__.pyi": "582b4a7ead62b2ae8605e17fa084c063", - "reflex/components/radix/themes/base.pyi": "fa9d8fa28255b259b91cebe363881b6c", - "reflex/components/radix/themes/color_mode.pyi": "dfbe926e30f4f1b013086c59def6a298", - "reflex/components/radix/themes/components/__init__.pyi": "efa279ee05479d7bb8a64d49da808d03", - "reflex/components/radix/themes/components/alert_dialog.pyi": "fd831007e357a398de254a7a67569394", - "reflex/components/radix/themes/components/aspect_ratio.pyi": "7087ecde3b3484a5e3cc77388228fc67", - "reflex/components/radix/themes/components/avatar.pyi": "093195106ff1a94b086fc98845d827e5", - "reflex/components/radix/themes/components/badge.pyi": "1ff3402150492bdbc1667a06f78de274", - "reflex/components/radix/themes/components/button.pyi": "1b919088eedbf5c5bae929c52b05fbd4", - "reflex/components/radix/themes/components/callout.pyi": "1b96bd57f15b28d6f371548452befde4", - "reflex/components/radix/themes/components/card.pyi": "c19d311c41f547b25afb6a224c3f885b", - "reflex/components/radix/themes/components/checkbox.pyi": "f7835e065a1c2961290320b5bb766d22", - "reflex/components/radix/themes/components/checkbox_cards.pyi": "339b3ed08900e4cb5e9786de6aa17fb8", - "reflex/components/radix/themes/components/checkbox_group.pyi": "1bdd96f70bcf9289db2a1f210de14b8b", - "reflex/components/radix/themes/components/context_menu.pyi": "84008b434ef1d349db155966e58c5f5a", - "reflex/components/radix/themes/components/data_list.pyi": "37df138ba88fe43f61648646f9d94616", - "reflex/components/radix/themes/components/dialog.pyi": "0d414930a1768b80cdbbf4c0b51c296a", - "reflex/components/radix/themes/components/dropdown_menu.pyi": "3aee89946b2ac137e536b9eb88c335bc", - "reflex/components/radix/themes/components/hover_card.pyi": "3ddc1bdf79f98d291c86553e275207aa", - "reflex/components/radix/themes/components/icon_button.pyi": "09f16042ed75fb605ba89df069f439de", - "reflex/components/radix/themes/components/inset.pyi": "2088b78fafc9256809b245d2544bb35a", - "reflex/components/radix/themes/components/popover.pyi": "80b410f0fcdfdd5815b06d17e8feff94", - "reflex/components/radix/themes/components/progress.pyi": "e4043f18f1eb33c0f675b827318d45ac", - "reflex/components/radix/themes/components/radio.pyi": "afc737e2f4c34eb9520361b1677c889f", - "reflex/components/radix/themes/components/radio_cards.pyi": "18775b80e37bfb7da81457f8068a83c3", - "reflex/components/radix/themes/components/radio_group.pyi": "b2b1491d0461acdd1980bf1b0ff9d0bc", - "reflex/components/radix/themes/components/scroll_area.pyi": "9aec5400dc3795f2430010bc288e9c03", - "reflex/components/radix/themes/components/segmented_control.pyi": "ed256e9e239c3378372dc5e25efe37e4", - "reflex/components/radix/themes/components/select.pyi": "58ca47be618155d427dbadefd5d72471", - "reflex/components/radix/themes/components/separator.pyi": "c84161d5924b4038289a3419f516582a", - "reflex/components/radix/themes/components/skeleton.pyi": "bde931806e05805957f06a618c675fc9", - "reflex/components/radix/themes/components/slider.pyi": "aa45010f4674390f055da20edd2008bb", - "reflex/components/radix/themes/components/spinner.pyi": "6080e9ba414077f78e52fd01c9061da6", - "reflex/components/radix/themes/components/switch.pyi": "fcb3767bda070ef0af4ee664f2f068a4", - "reflex/components/radix/themes/components/table.pyi": "1c5aca792b0acb1a79d8365f64e76e59", - "reflex/components/radix/themes/components/tabs.pyi": "94029d5505bc14a7059633472a4908e0", - "reflex/components/radix/themes/components/text_area.pyi": "ccc7f3f38218ee3189baf1350ec64b40", - "reflex/components/radix/themes/components/text_field.pyi": "87526c9942ec517140bd4fbb86f89600", - "reflex/components/radix/themes/components/tooltip.pyi": "6ada2d6bc3abc9aa5e55d66f81ae95de", - "reflex/components/radix/themes/layout/__init__.pyi": "73eefc509a49215b1797b5b5d28d035e", - "reflex/components/radix/themes/layout/base.pyi": "091d8353514b73ef0dc4a5a9347b0fdf", - "reflex/components/radix/themes/layout/box.pyi": "1467e1ef4b6722b728596f42806d693b", - "reflex/components/radix/themes/layout/center.pyi": "a0c20b842b2fb7407b7a3d079aaa1dae", - "reflex/components/radix/themes/layout/container.pyi": "6a8a5581f2bad13bbac35d6e0999074f", - "reflex/components/radix/themes/layout/flex.pyi": "446e10f5fb7c55855eb240d6e3f73dfa", - "reflex/components/radix/themes/layout/grid.pyi": "f2979297d7e4263b578dab47b71aa5c2", - "reflex/components/radix/themes/layout/list.pyi": "d14e480c72efe8c64d37408b789b9472", - "reflex/components/radix/themes/layout/section.pyi": "23ae5def80148fed4b5e729c9d931326", - "reflex/components/radix/themes/layout/spacer.pyi": "ce2020374844384dac1b7c18e00fd0a5", - "reflex/components/radix/themes/layout/stack.pyi": "6271d05ef431208d8db0418f04c6987c", - "reflex/components/radix/themes/typography/__init__.pyi": "b8ef970530397e9984004961f3aaee62", - "reflex/components/radix/themes/typography/blockquote.pyi": "dbdf3fdf7cdca84447a8007dd43b9d78", - "reflex/components/radix/themes/typography/code.pyi": "c5b95158b8328a199120e4795bf27f44", - "reflex/components/radix/themes/typography/heading.pyi": "a5cf5efcf485fd8a6f1b38a64e27a52d", - "reflex/components/radix/themes/typography/link.pyi": "64357025c9d5bae85ea206dcd3ebfa2d", - "reflex/components/radix/themes/typography/text.pyi": "0e1434318bb482157b8d1b276550019b", - "reflex/components/react_player/audio.pyi": "0e1690ff1f1f39bc748278d292238350", - "reflex/components/react_player/react_player.pyi": "5289c57db568ee3ae01f97789c445f37", - "reflex/components/react_player/video.pyi": "998671c06103d797c554d9278eb3b2a0", - "reflex/components/react_router/dom.pyi": "2198359c2d8f3d1856f4391978a4e2de", - "reflex/components/recharts/__init__.pyi": "6ee7f1ca2c0912f389ba6f3251a74d99", - "reflex/components/recharts/cartesian.pyi": "4dc01da3195f80b9408d84373b874c41", - "reflex/components/recharts/charts.pyi": "16cd435d77f06f0315b595b7e62cf44b", - "reflex/components/recharts/general.pyi": "9abf71810a5405fd45b13804c0a7fd1a", - "reflex/components/recharts/polar.pyi": "ea4743e8903365ba95bc4b653c47cc4a", - "reflex/components/recharts/recharts.pyi": "b3d93d085d51053bbb8f65326f34a299", - "reflex/components/sonner/toast.pyi": "636050fcc919f8ab0903c30dceaa18f1" + "packages/reflex-components-code/src/reflex_components_code/code.pyi": "c867c4384ade3ea5d97ba794966af140", + "packages/reflex-components-code/src/reflex_components_code/shiki_code_block.pyi": "8dcccd4551a7b6ff3280f826f97f2aca", + "packages/reflex-components-core/src/reflex_components_core/__init__.pyi": "6c3ceff429117483dd0035e7a21930f4", + "packages/reflex-components-core/src/reflex_components_core/base/__init__.pyi": "4beb5ba739680b7974c37241f3f6791c", + "packages/reflex-components-core/src/reflex_components_core/base/app_wrap.pyi": "f8cb9cee04ffa9006d74db1b3500aadf", + "packages/reflex-components-core/src/reflex_components_core/base/body.pyi": "3ad765bf23f5da9cee280b44a80dbcc5", + "packages/reflex-components-core/src/reflex_components_core/base/document.pyi": "ca67174aeb6419d320b6c54a1cdd1955", + "packages/reflex-components-core/src/reflex_components_core/base/error_boundary.pyi": "19dcd8cba0603b6371d24d768eaf84f4", + "packages/reflex-components-core/src/reflex_components_core/base/fragment.pyi": "75a5f1a9ac628f3f211cd7d72c605aa2", + "packages/reflex-components-core/src/reflex_components_core/base/link.pyi": "8893f124131be7c2f60f086fa4beb255", + "packages/reflex-components-core/src/reflex_components_core/base/meta.pyi": "1a1904b9118c22fdae1b5e70e5006aa7", + "packages/reflex-components-core/src/reflex_components_core/base/script.pyi": "58bae1b5f9d6fabd284752e0adf8d868", + "packages/reflex-components-core/src/reflex_components_core/base/strict_mode.pyi": "a037c6fcb007d884915e52a6ab0046af", + "packages/reflex-components-core/src/reflex_components_core/core/__init__.pyi": "17f79762cef09c69acd9df227cf3bb35", + "packages/reflex-components-core/src/reflex_components_core/core/auto_scroll.pyi": "59a02f780bd1568c26f7991c99bf2110", + "packages/reflex-components-core/src/reflex_components_core/core/banner.pyi": "eab4a1e0b86d546a324ec1814fb86de0", + "packages/reflex-components-core/src/reflex_components_core/core/clipboard.pyi": "3bf314f1e3a1faf9c060cd34f8fa7079", + "packages/reflex-components-core/src/reflex_components_core/core/debounce.pyi": "dab0e776a0fbe8c2525ed8950bdf0293", + "packages/reflex-components-core/src/reflex_components_core/core/helmet.pyi": "9fa0dea0b63a975dafe2b4c06c5a008c", + "packages/reflex-components-core/src/reflex_components_core/core/html.pyi": "f3d8fe298d93bc20aee9d6461f7d5b17", + "packages/reflex-components-core/src/reflex_components_core/core/sticky.pyi": "3bca0ab6a4b0f36e8adf6e8a144e403c", + "packages/reflex-components-core/src/reflex_components_core/core/upload.pyi": "d76f2abf2acc8b1b0a01f1e46a906794", + "packages/reflex-components-core/src/reflex_components_core/core/window_events.pyi": "22dbc116b98a9e70a36cb1ef846adc62", + "packages/reflex-components-core/src/reflex_components_core/datadisplay/__init__.pyi": "73e27e0bfd7e914c4baba7b166e75736", + "packages/reflex-components-core/src/reflex_components_core/el/__init__.pyi": "a1d4d65ddc73b5fbba3726f4bcf6d6f0", + "packages/reflex-components-core/src/reflex_components_core/el/element.pyi": "0cb57beaf2a3dd03be9d3b008bb0a916", + "packages/reflex-components-core/src/reflex_components_core/el/elements/__init__.pyi": "fbe4c6f4960bcd811311b1f73987cdb1", + "packages/reflex-components-core/src/reflex_components_core/el/elements/base.pyi": "7220e4f079af9f3473491751a4cacef1", + "packages/reflex-components-core/src/reflex_components_core/el/elements/forms.pyi": "da68f5ced0afa890ad79e646aecc094a", + "packages/reflex-components-core/src/reflex_components_core/el/elements/inline.pyi": "d0d132e32abe141ca2940637609875bd", + "packages/reflex-components-core/src/reflex_components_core/el/elements/media.pyi": "77703a560c2aa2f013ae58e28c6b0c88", + "packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.pyi": "cc4c5bd0128af7b98dc44923c37739b6", + "packages/reflex-components-core/src/reflex_components_core/el/elements/other.pyi": "8f775db675bb055cd8c75c4ea00892f9", + "packages/reflex-components-core/src/reflex_components_core/el/elements/scripts.pyi": "6c3925340de81d4d26d1de74c5ed24e9", + "packages/reflex-components-core/src/reflex_components_core/el/elements/sectioning.pyi": "919119139837bd01475e90173602c381", + "packages/reflex-components-core/src/reflex_components_core/el/elements/tables.pyi": "8b7933cb9f4c11734f49ec41f2b80553", + "packages/reflex-components-core/src/reflex_components_core/el/elements/typography.pyi": "f5cfe2996ae3aaf7bd630c852c0e9068", + "packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.pyi": "0f8ec6d513cc649caf10981cf29bcd38", + "packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.pyi": "3dcdc432507f1a455b97f719160118ac", + "packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "21ac24351c0e0de123dec46a7e90209b", + "packages/reflex-components-markdown/src/reflex_components_markdown/markdown.pyi": "2968ff9eaf788b5a93fa5c4d6d5d818c", + "packages/reflex-components-moment/src/reflex_components_moment/moment.pyi": "dffd48bde7241e80093b2f8143cfc30e", + "packages/reflex-components-plotly/src/reflex_components_plotly/plotly.pyi": "84b5f3f01d5fcb60067be9105a4a12ba", + "packages/reflex-components-radix/src/reflex_components_radix/__init__.pyi": "899d63be42eb21c187519920518bf32e", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/__init__.pyi": "11bc17db31334f11d7fcde315b29cae1", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/accordion.pyi": "e3cbfe19cadd7e24e7a3556b898e9170", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/base.pyi": "4ed7151c05f155a7140b608e3350b684", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/dialog.pyi": "75539bdf369af2bab188335a0cad59f0", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/drawer.pyi": "8eed776a75cc0583a5a27e12205fc22b", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/form.pyi": "47f6c48ecd0733b6183af90a65797f45", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/progress.pyi": "5181780f01dcb17a996d383a9fbda0e1", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/slider.pyi": "dfeab92ef06442a0339eed625c8902f8", + "packages/reflex-components-radix/src/reflex_components_radix/themes/__init__.pyi": "a152450345e7da9124e954843224e936", + "packages/reflex-components-radix/src/reflex_components_radix/themes/base.pyi": "678774944ad675c1efad0dcd5ac26097", + "packages/reflex-components-radix/src/reflex_components_radix/themes/color_mode.pyi": "cdf329dd430dfb35bc0fc0c22903d2bf", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/__init__.pyi": "67d45aa96a97359ee556725641bb583f", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/alert_dialog.pyi": "4f332c3ca24a26d2349503689eb89c8c", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/aspect_ratio.pyi": "559909085f8c1cf37fad1c4ecd2502d7", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/avatar.pyi": "c8730ae9413528fc88caeb0a013e60ee", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/badge.pyi": "9227edf16541a8f0e413efb1d9cdca42", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/button.pyi": "6114e8e6330932100884e435544a3a52", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/callout.pyi": "d356e8555fa4a3141783827355f66524", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/card.pyi": "05e5b293b2d5388d619c5b4676609950", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox.pyi": "3d1439b90dbc2b1d4995e7ed91e040f3", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_cards.pyi": "0f5736c128c8e6a7cf7a3c7eef1e7159", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_group.pyi": "19042165e9ff53de6be5e8c18e61429f", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/context_menu.pyi": "0affc657216038379632808dcbbec1f5", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/data_list.pyi": "86fa8b592994d23e99d585d64056d0c1", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/dialog.pyi": "3cfc6ed392463054db7b41db012af862", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/dropdown_menu.pyi": "da1560563223edcb56cf8436ab1f5ef6", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/hover_card.pyi": "557e3cb9553c13fd3d2bae7c30371176", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/icon_button.pyi": "632ee951c76dc22bf63c904d2cbc74a7", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/inset.pyi": "a1e3374cda158ea64e180da6ce61d265", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/popover.pyi": "1442682c3b937f79ef84737ac5464245", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/progress.pyi": "2d064ffabbc2ed3a23e8f46d833b0277", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio.pyi": "a331ef9d0d869a997562d9f987e1814f", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_cards.pyi": "4c236e3c3d1ed17ce062cd63a3eb613f", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_group.pyi": "0355e29cf569e1405091bb29f525e86b", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/scroll_area.pyi": "5b7223cab1ac1ac42bdeeab6cd72bd8a", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/segmented_control.pyi": "d7451983f9591ff7b81f3bfbd72a2689", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/select.pyi": "791cb1c3c5c67f77bd581c6d0b99ab9b", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/separator.pyi": "00a858bbdc0d35f2220775eb06d2d733", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/skeleton.pyi": "fd31c794583ec744207312fa1a72c3d1", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/slider.pyi": "683128339dfe213d8a7401b75814953b", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/spinner.pyi": "713adb1481b8e3c525fd054d417239b0", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/switch.pyi": "7e038a571514d7ddedba423797498b4c", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/table.pyi": "1bdfc7f3f18894378a5629c3d16546fc", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/tabs.pyi": "079c79809feaea387c07716083184c94", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_area.pyi": "36d3ecbef7a6b28554f400fc41433522", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_field.pyi": "be36d108861eb1c5d486175637bd78d1", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/tooltip.pyi": "a68e09331535bbf6187d1efbd248baf1", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/__init__.pyi": "b7dbdecab770a7fc1435ad417d7f91c1", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/base.pyi": "a63c001cf714739c04a86556951e9bfc", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/box.pyi": "17f83beeb3c6c83b1571ea5d4887f76c", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/center.pyi": "123b2d713d9845f2715576d5cf9f2536", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/container.pyi": "fe41ccaa90e859f2f8a74b015fbde058", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/flex.pyi": "f615a989fcfbda790de3f1305c42605a", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/grid.pyi": "2a23d9b1d7f2098f3e05e6deeb7453fb", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/list.pyi": "07bd0842f489c9cd4f1c6e3800cc7afd", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/section.pyi": "edada2fc5d0139b61d035485ecaf5ebb", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/spacer.pyi": "a289f92e5da0ec4576e4c0bc1ee4986c", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/stack.pyi": "b857d2ed0549a217fa28e3cb86c52ffb", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/__init__.pyi": "15b13877fc52b82182fc42acd8adb027", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/blockquote.pyi": "833c57bded556a13db59726769183230", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/code.pyi": "0ea298c8016e29b3343d708c43a66179", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/heading.pyi": "e38ca88110e315303c18d615556d1542", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/link.pyi": "edaf5f2f316bd35b6d32f7a369d5fad6", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/text.pyi": "c6bf9ba91a7650d6742773be94bdde33", + "packages/reflex-components-react-player/src/reflex_components_react_player/audio.pyi": "383e6c81567915da364dd7c02705dd7c", + "packages/reflex-components-react-player/src/reflex_components_react_player/react_player.pyi": "20deae7c420096252c8958f1d9194b9e", + "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "58e2a086560862b2f9ecd1f18840e893", + "packages/reflex-components-react-router/src/reflex_components_react_router/dom.pyi": "41c140176951d4536af9d51aa22da96e", + "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "1f04ab9f482261f18010fbf728d7b946", + "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "cb57586301f693b6667e29e916a34427", + "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "e6e69455c92b91a8cd37cec7d83b3589", + "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "6bdf04181af6b94f06ebc07e32649a13", + "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "7af2bc6774ef1c5240165a6afff12633", + "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "83a840eceb0515ce7d27d52644ff60a5", + "packages/reflex-components-sonner/src/reflex_components_sonner/toast.pyi": "296cb0d27bc1774f3a330ca9f79fe2bd", + "reflex/__init__.pyi": "22a843faeeedcfcda493b34727b6e957", + "reflex/components/__init__.pyi": "d620d52015f908cda828d231c1064236" } diff --git a/pyproject.toml b/pyproject.toml index ea7de088842..bd8849d903f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "reflex" -version = "0.9.0dev1" +dynamic = ["version"] description = "Web apps in pure Python." license.text = "Apache-2.0" authors = [ @@ -37,6 +37,19 @@ dependencies = [ "starlette >=0.47.0", "typing_extensions >=4.13.0", "wrapt >=1.17.0,<3.0", + "reflex-components-code", + "reflex-components-core", + "reflex-components-dataeditor", + "reflex-components-gridjs", + "reflex-components-lucide", + "reflex-components-markdown", + "reflex-components-moment", + "reflex-components-plotly", + "reflex-components-radix", + "reflex-components-react-player", + "reflex-components-react-router", + "reflex-components-recharts", + "reflex-components-sonner", ] classifiers = [ @@ -102,9 +115,15 @@ dev = [ [build-system] -requires = ["hatchling"] +requires = ["hatchling", "uv-dynamic-versioning"] build-backend = "hatchling.build" +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +fallback-version = "0.0.0dev0" + [tool.hatch.build] include = ["reflex", "scripts/hatch_build.py"] targets.sdist.artifacts = ["*.pyi"] @@ -116,6 +135,21 @@ dependencies = ["plotly", "ruff", "pre_commit", "toml"] require-runtime-dependencies = true [tool.pyright] +extraPaths = [ + "packages/reflex-code/src", + "packages/reflex-components/src", + "packages/reflex-dataeditor/src", + "packages/reflex-gridjs/src", + "packages/reflex-lucide/src", + "packages/reflex-markdown/src", + "packages/reflex-moment/src", + "packages/reflex-plotly/src", + "packages/reflex-radix/src", + "packages/reflex-react-player/src", + "packages/reflex-react-router/src", + "packages/reflex-recharts/src", + "packages/reflex-sonner/src", +] reportIncompatibleMethodOverride = false [tool.ruff] @@ -214,7 +248,7 @@ omit = [ [tool.coverage.report] show_missing = true # TODO bump back to 79 -fail_under = 70 +fail_under = 67 precision = 2 ignore_errors = true @@ -279,13 +313,22 @@ rev = "v1.1.408" hooks = [{ id = "pyright", args = ["reflex", "tests"], language = "system" }] [[tool.pre-commit.repos]] -repo = "https://github.com/pre-commit/mirrors-prettier" -rev = "f62a70a3a7114896b062de517d72829ea1c884b6" -hooks = [{ id = "prettier", require_serial = true }] +repo = "https://github.com/biomejs/pre-commit" +rev = "v0.6.1" +hooks = [ + { id = "biome-format", args = [ + "--indent-width", + "2", + "--indent-style", + "space", + ], additional_dependencies = [ + "@biomejs/biome@2.4.8", + ] }, +] [tool.uv] required-version = ">=0.7.0" -sources = { reflex-docgen = { workspace = true } } +sources = { reflex-components-code = { workspace = true }, reflex-components-core = { workspace = true }, reflex-components-dataeditor = { workspace = true }, reflex-components-gridjs = { workspace = true }, reflex-components-lucide = { workspace = true }, reflex-components-markdown = { workspace = true }, reflex-components-moment = { workspace = true }, reflex-components-plotly = { workspace = true }, reflex-components-radix = { workspace = true }, reflex-components-react-player = { workspace = true }, reflex-components-react-router = { workspace = true }, reflex-components-recharts = { workspace = true }, reflex-components-sonner = { workspace = true }, reflex-docgen = { workspace = true } } [tool.uv.workspace] members = ["packages/*"] diff --git a/reflex/app.py b/reflex/app.py index 39fd6478797..85415e49c0b 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -31,6 +31,19 @@ from types import SimpleNamespace from typing import TYPE_CHECKING, Any, BinaryIO, ParamSpec, get_args, get_type_hints +from reflex_components_core.base.app_wrap import AppWrap +from reflex_components_core.base.error_boundary import ErrorBoundary +from reflex_components_core.base.fragment import Fragment +from reflex_components_core.base.strict_mode import StrictMode +from reflex_components_core.core.banner import ( + backend_disabled, + connection_pulser, + connection_toaster, +) +from reflex_components_core.core.breakpoints import set_breakpoints +from reflex_components_core.core.sticky import sticky +from reflex_components_radix import themes +from reflex_components_sonner.toast import toast from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn from socketio import ASGIApp as EngineIOApp from socketio import AsyncNamespace, AsyncServer @@ -54,25 +67,12 @@ compile_theme, readable_name_from_component, ) -from reflex.components.base.app_wrap import AppWrap -from reflex.components.base.error_boundary import ErrorBoundary -from reflex.components.base.fragment import Fragment -from reflex.components.base.strict_mode import StrictMode from reflex.components.component import ( CUSTOM_COMPONENTS, Component, ComponentStyle, evaluate_style_namespaces, ) -from reflex.components.core.banner import ( - backend_disabled, - connection_pulser, - connection_toaster, -) -from reflex.components.core.breakpoints import set_breakpoints -from reflex.components.core.sticky import sticky -from reflex.components.radix import themes -from reflex.components.sonner.toast import toast from reflex.config import get_config from reflex.environment import ExecutorType, environment from reflex.event import ( @@ -156,7 +156,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec: EventSpec: The window alert event. """ - from reflex.components.sonner.toast import toast + from reflex_components_sonner.toast import toast error = traceback.format_exc() @@ -696,7 +696,7 @@ def _add_default_endpoints(self): def _add_optional_endpoints(self): """Add optional api endpoints (_upload).""" - from reflex.components.core.upload import Upload, get_upload_dir + from reflex_components_core.core.upload import Upload, get_upload_dir if not self._api: return @@ -815,7 +815,7 @@ def add_page( if route == constants.Page404.SLUG: if component is None: - from reflex.components.el.elements import span + from reflex_components_core.el.elements import span component = span("404: Page not found") component = self._generate_component(component) diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index a097b2d99f2..7737bf77ca1 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -8,9 +8,10 @@ from pathlib import Path from typing import TYPE_CHECKING, Any +from reflex_components_core.base.fragment import Fragment + from reflex import constants from reflex.compiler import templates, utils -from reflex.components.base.fragment import Fragment from reflex.components.component import ( BaseComponent, Component, diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index f02edacbd71..c5fc117566c 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -12,14 +12,15 @@ from typing import Any, TypedDict from urllib.parse import urlparse +from reflex_components_core.base import Description, Image, Scripts +from reflex_components_core.base.document import Links, ScrollRestoration +from reflex_components_core.base.document import Meta as ReactMeta +from reflex_components_core.el.elements.metadata import Head, Link, Meta, Title +from reflex_components_core.el.elements.other import Html +from reflex_components_core.el.elements.sectioning import Body + from reflex import constants -from reflex.components.base import Description, Image, Scripts -from reflex.components.base.document import Links, ScrollRestoration -from reflex.components.base.document import Meta as ReactMeta from reflex.components.component import Component, ComponentStyle, CustomComponent -from reflex.components.el.elements.metadata import Head, Link, Meta, Title -from reflex.components.el.elements.other import Html -from reflex.components.el.elements.sectioning import Body from reflex.constants.state import FIELD_MARKER from reflex.istate.storage import Cookie, LocalStorage, SessionStorage from reflex.state import BaseState, _resolve_delta diff --git a/reflex/components/__init__.py b/reflex/components/__init__.py index 36364a3b851..b44e3e4fdfd 100644 --- a/reflex/components/__init__.py +++ b/reflex/components/__init__.py @@ -1,34 +1,145 @@ -"""Import all the components.""" +"""Import all the components. + +Components have been split across multiple packages. +This module installs an import redirect hook so that +``from reflex.components. import X`` continues to work +by delegating to the appropriate package. +""" from __future__ import annotations +import importlib +import importlib.abc +import importlib.machinery +import sys +from types import ModuleType + from reflex.utils import lazy_loader -_SUBMODULES: set[str] = { - "lucide", - "core", - "datadisplay", - "gridjs", - "markdown", - "moment", - "plotly", - "radix", - "react_player", - "react_router", - "sonner", - "el", - "base", - "recharts", +# Mapping from subpackage name to the target top-level package. +_SUBPACKAGE_TARGETS: dict[str, str] = { + # reflex-components (base package) + "base": "reflex_components_core.base", + "core": "reflex_components_core.core", + "datadisplay": "reflex_components_core.datadisplay", + "el": "reflex_components_core.el", + # Standalone packages + "gridjs": "reflex_components_gridjs", + "lucide": "reflex_components_lucide", + "markdown": "reflex_components_markdown", + "moment": "reflex_components_moment", + "plotly": "reflex_components_plotly", + "radix": "reflex_components_radix", + "react_player": "reflex_components_react_player", + "react_router": "reflex_components_react_router", + "recharts": "reflex_components_recharts", + "sonner": "reflex_components_sonner", +} + +# Deeper overrides for subpackages that were split from datadisplay. +# Checked before the general _SUBPACKAGE_TARGETS mapping. +_DEEP_OVERRIDES: dict[str, str] = { + "datadisplay.code": "reflex_components_code.code", + "datadisplay.shiki_code_block": "reflex_components_code.shiki_code_block", + "datadisplay.dataeditor": "reflex_components_dataeditor.dataeditor", } + +class _AliasLoader(importlib.abc.Loader): + """Loader that aliases one module name to another.""" + + def __init__(self, target_name: str): + self.target_name = target_name + + def create_module(self, spec: importlib.machinery.ModuleSpec) -> ModuleType | None: + return None + + def exec_module(self, module: ModuleType) -> None: + target = importlib.import_module(self.target_name) + # Make the alias point to the real module. + module.__dict__.update(target.__dict__) + module.__path__ = getattr(target, "__path__", []) + module.__file__ = getattr(target, "__file__", None) + module.__loader__ = self + # Register the target module under the alias name so subsequent + # imports resolve immediately. + sys.modules[module.__name__] = target + + +class _ComponentsRedirect(importlib.abc.MetaPathFinder): + """Import hook: redirects ``reflex.components.`` to the real package.""" + + def find_spec( + self, + fullname: str, + path: object = None, + target: object = None, + ) -> importlib.machinery.ModuleSpec | None: + parts = fullname.split(".") + if len(parts) >= 3 and parts[0] == "reflex" and parts[1] == "components": + subpkg = parts[2] + rest_parts = parts[3:] + + # Check deep overrides first (e.g. datadisplay.code -> reflex_components_code.code). + if rest_parts: + deep_key = f"{subpkg}.{rest_parts[0]}" + override = _DEEP_OVERRIDES.get(deep_key) + if override is not None: + extra = ".".join(rest_parts[1:]) + target_name = f"{override}.{extra}" if extra else override + return importlib.machinery.ModuleSpec( + fullname, + _AliasLoader(target_name), + is_package=True, + ) + + # General subpackage mapping. + if subpkg in _SUBPACKAGE_TARGETS: + base = _SUBPACKAGE_TARGETS[subpkg] + rest = ".".join(rest_parts) + target_name = f"{base}.{rest}" if rest else base + return importlib.machinery.ModuleSpec( + fullname, + _AliasLoader(target_name), + is_package=True, + ) + return None + + +# Install the import redirect hook. +if not any(isinstance(f, _ComponentsRedirect) for f in sys.meta_path): + sys.meta_path.insert(0, _ComponentsRedirect()) + + +# Submodules that still live in reflex.components (infrastructure). _SUBMOD_ATTRS: dict[str, list[str]] = { "component": [ "Component", "NoSSRComponent", ], } -__getattr__, __dir__, __all__ = lazy_loader.attach( + +_lazy_getattr, __dir__, __all__ = lazy_loader.attach( __name__, - submodules=_SUBMODULES, + submodules=set(), submod_attrs=_SUBMOD_ATTRS, ) + + +def __getattr__(name: str) -> object: + """Resolve attributes: first try local lazy loader, then delegate to component packages. + + Returns: + The requested attribute from this module or a component package. + + Raises: + AttributeError: If the attribute is not found. + """ + try: + return _lazy_getattr(name) + except AttributeError: + pass + if name in _SUBPACKAGE_TARGETS: + return importlib.import_module(_SUBPACKAGE_TARGETS[name]) + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) diff --git a/reflex/components/component.py b/reflex/components/component.py index fbdc1decfda..673a5cb012a 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -17,13 +17,13 @@ from types import SimpleNamespace from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast, get_args, get_origin +from reflex_components_core.core.breakpoints import Breakpoints from rich.markup import escape from typing_extensions import dataclass_transform import reflex.state from reflex import constants from reflex.compiler.templates import stateful_component_template -from reflex.components.core.breakpoints import Breakpoints from reflex.components.dynamic import load_dynamic_serializer from reflex.components.field import BaseField, FieldBasedMeta from reflex.components.tags import Tag @@ -1140,8 +1140,8 @@ def create(cls: type[T], *children, **props) -> T: The component. """ # Import here to avoid circular imports. - from reflex.components.base.bare import Bare - from reflex.components.base.fragment import Fragment + from reflex_components_core.base.bare import Bare + from reflex_components_core.base.fragment import Fragment # Filter out None props props = {key: value for key, value in props.items() if value is not None} @@ -1350,10 +1350,10 @@ def _validate_component_children(self, children: list[Component]): children: The children of the component. """ - from reflex.components.base.fragment import Fragment - from reflex.components.core.cond import Cond - from reflex.components.core.foreach import Foreach - from reflex.components.core.match import Match + from reflex_components_core.base.fragment import Fragment + from reflex_components_core.core.cond import Cond + from reflex_components_core.core.foreach import Foreach + from reflex_components_core.core.match import Match no_valid_parents_defined = all(child._valid_parents == [] for child in children) if ( @@ -2417,7 +2417,7 @@ def create(cls, component: Component) -> StatefulComponent | None: Returns: The stateful component or None if the component should not be memoized. """ - from reflex.components.core.foreach import Foreach + from reflex_components_core.core.foreach import Foreach if component._memoization_mode.disposition == MemoizationDisposition.NEVER: # Never memoize this component. @@ -2498,10 +2498,10 @@ def _child_var(child: Component) -> Var | Component: Returns: The Var from the child component or the child itself (for regular cases). """ - from reflex.components.base.bare import Bare - from reflex.components.core.cond import Cond - from reflex.components.core.foreach import Foreach - from reflex.components.core.match import Match + from reflex_components_core.base.bare import Bare + from reflex_components_core.core.cond import Cond + from reflex_components_core.core.foreach import Foreach + from reflex_components_core.core.match import Match if isinstance(child, Bare): return child.contents @@ -2851,7 +2851,7 @@ def empty_component() -> Component: Returns: An empty component. """ - from reflex.components.base.bare import Bare + from reflex_components_core.base.bare import Bare return Bare.create("") diff --git a/reflex/components/dynamic.py b/reflex/components/dynamic.py index 0b98a8844b0..c6d4ff9ad97 100644 --- a/reflex/components/dynamic.py +++ b/reflex/components/dynamic.py @@ -70,8 +70,9 @@ def make_component(component: Component) -> str: The generated code """ # Causes a circular import, so we import here. + from reflex_components_core.base.bare import Bare + from reflex.compiler import compiler, templates, utils - from reflex.components.base.bare import Bare component = Bare.create(Var.create(component)) diff --git a/reflex/components/tags/iter_tag.py b/reflex/components/tags/iter_tag.py index 64b7e1e9ad2..d268636e74f 100644 --- a/reflex/components/tags/iter_tag.py +++ b/reflex/components/tags/iter_tag.py @@ -79,10 +79,11 @@ def render_component(self) -> Component: ValueError: If the render function doesn't return a component. """ # Import here to avoid circular imports. + from reflex_components_core.base.fragment import Fragment + from reflex_components_core.core.cond import Cond + from reflex_components_core.core.foreach import Foreach + from reflex.compiler.compiler import _into_component_once - from reflex.components.base.fragment import Fragment - from reflex.components.core.cond import Cond - from reflex.components.core.foreach import Foreach # Get the render function arguments. args = inspect.getfullargspec(self.render_fn).args diff --git a/reflex/config.py b/reflex/config.py index 56e23fd13a0..f3f17aa2949 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -176,7 +176,7 @@ class BaseConfig: state_auto_setters: Whether to automatically create setters for state base vars. show_built_with_reflex: Whether to display the sticky "Built with Reflex" badge on all pages. is_reflex_cloud: Whether the app is running in the reflex cloud environment. - extra_overlay_function: Extra overlay function to run after the app is built. Formatted such that `from path_0.path_1... import path[-1]`, and calling it with no arguments would work. For example, "reflex.components.moment.moment". + extra_overlay_function: Extra overlay function to run after the app is built. Formatted such that `from path_0.path_1... import path[-1]`, and calling it with no arguments would work. For example, "reflex_components_moment.moment". plugins: List of plugins to use in the app. disable_plugins: List of plugin types to disable in the app. transport: The transport method for client-server communication. diff --git a/reflex/constants/colors.py b/reflex/constants/colors.py index f769f8099b6..a318eca990f 100644 --- a/reflex/constants/colors.py +++ b/reflex/constants/colors.py @@ -68,7 +68,7 @@ def format_color( if isinstance(alpha, bool): return f"var(--{color}-{'a' if alpha else ''}{shade})" - from reflex.components.core import cond + from reflex_components_core.core import cond alpha_var = cond(alpha, "a", "") return f"var(--{color}-{alpha_var}{shade})" diff --git a/reflex/event.py b/reflex/event.py index 959c57aa11e..75ccf80e9d1 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -880,7 +880,7 @@ def as_event_spec(self, handler: EventHandler) -> EventSpec: Raises: ValueError: If the on_upload_progress is not a valid event handler. """ - from reflex.components.core.upload import ( + from reflex_components_core.core.upload import ( DEFAULT_UPLOAD_ID, upload_files_context_var_data, ) @@ -1258,7 +1258,7 @@ def download( ValueError: If the URL provided is invalid, both URL and data are provided, or the data is not an expected type. """ - from reflex.components.core.cond import cond + from reflex_components_core.core.cond import cond if isinstance(url, str): if not url.startswith("/"): diff --git a/reflex/experimental/__init__.py b/reflex/experimental/__init__.py index 2734da19112..09905906a7c 100644 --- a/reflex/experimental/__init__.py +++ b/reflex/experimental/__init__.py @@ -2,7 +2,8 @@ from types import SimpleNamespace -from reflex.components.datadisplay.shiki_code_block import code_block as code_block +from reflex_components_code.shiki_code_block import code_block as code_block + from reflex.utils.console import warn from reflex.utils.misc import run_in_thread diff --git a/reflex/state.py b/reflex/state.py index a105f9039d4..affe7324376 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -2554,7 +2554,7 @@ def dynamic(func: Callable[[T], Component]): state_class: type[T] = values[0] def wrapper() -> Component: - from reflex.components.base.fragment import fragment + from reflex_components_core.base.fragment import fragment return fragment(state_class._evaluate(lambda state: func(state))) diff --git a/reflex/style.py b/reflex/style.py index 26cc4955f85..d25b059f181 100644 --- a/reflex/style.py +++ b/reflex/style.py @@ -5,8 +5,9 @@ from collections.abc import Mapping from typing import Any, Literal +from reflex_components_core.core.breakpoints import Breakpoints, breakpoints_values + from reflex import constants -from reflex.components.core.breakpoints import Breakpoints, breakpoints_values from reflex.event import EventChain, EventHandler, EventSpec, run_script from reflex.utils import format from reflex.utils.exceptions import ReflexError diff --git a/reflex/utils/codespaces.py b/reflex/utils/codespaces.py index 03954911952..c0decaf5b5c 100644 --- a/reflex/utils/codespaces.py +++ b/reflex/utils/codespaces.py @@ -4,13 +4,13 @@ import os +from reflex_components_core.base.script import Script +from reflex_components_core.core.banner import has_connection_errors +from reflex_components_core.core.cond import cond from starlette.requests import Request from starlette.responses import HTMLResponse -from reflex.components.base.script import Script from reflex.components.component import Component -from reflex.components.core.banner import has_connection_errors -from reflex.components.core.cond import cond from reflex.constants import Endpoint from reflex.utils.decorator import once diff --git a/reflex/utils/misc.py b/reflex/utils/misc.py index 0eb4d245771..67ae79a13ab 100644 --- a/reflex/utils/misc.py +++ b/reflex/utils/misc.py @@ -103,7 +103,7 @@ def preload_color_theme(): Returns: Script: A script component to add to App.head_components """ - from reflex.components.el.elements.scripts import Script + from reflex_components_core.el.elements.scripts import Script # Create direct inline script content (like next-themes dangerouslySetInnerHTML) script_content = """ diff --git a/reflex/utils/pyi_generator.py b/reflex/utils/pyi_generator.py index 3dae7ff4c62..355a5250cd6 100644 --- a/reflex/utils/pyi_generator.py +++ b/reflex/utils/pyi_generator.py @@ -83,7 +83,7 @@ # TODO: fix import ordering and unused imports with ruff later DEFAULT_IMPORTS = { "typing": sorted(DEFAULT_TYPING_IMPORTS), - "reflex.components.core.breakpoints": ["Breakpoints"], + "reflex_components_core.core.breakpoints": ["Breakpoints"], "reflex.event": [ "EventChain", "EventHandler", @@ -1240,6 +1240,58 @@ def _write_pyi_file(module_path: Path, source: str) -> str: return md5(pyi_content.encode()).hexdigest() +# Mapping from component subpackage name to its target Python package. +_COMPONENT_SUBPACKAGE_TARGETS: dict[str, str] = { + # reflex-components (base package) + "base": "reflex_components_core.base", + "core": "reflex_components_core.core", + "datadisplay": "reflex_components_core.datadisplay", + "el": "reflex_components_core.el", + "gridjs": "reflex_components_gridjs", + "lucide": "reflex_components_lucide", + "moment": "reflex_components_moment", + # Deep overrides (datadisplay split) + "datadisplay.code": "reflex_components_code.code", + "datadisplay.shiki_code_block": "reflex_components_code.shiki_code_block", + "datadisplay.dataeditor": "reflex_components_dataeditor.dataeditor", + # Standalone packages + "markdown": "reflex_components_markdown", + "plotly": "reflex_components_plotly", + "radix": "reflex_components_radix", + "react_player": "reflex_components_react_player", + "react_router": "reflex_components_react_router", + "recharts": "reflex_components_recharts", + "sonner": "reflex_components_sonner", +} + + +def _rewrite_component_import(module: str) -> str: + """Rewrite a lazy-loader module path to the correct absolute package import. + + Args: + module: The module path from ``_SUBMOD_ATTRS`` (e.g. ``"components.radix.themes.base"``). + + Returns: + An absolute import path (``"reflex_components_radix.themes.base"``) for moved + components, or a relative path (``".components.component"``) for everything else. + """ + if module == "components": + # "components": ["el", "radix", ...] — these are re-exported submodules. + # Can't map to a single package, but the pyi generator handles each attr individually. + return "reflex_components_core" + if module.startswith("components."): + rest = module[len("components.") :] + # Try progressively deeper matches (e.g. "datadisplay.code" before "datadisplay"). + parts = rest.split(".") + for depth in range(min(len(parts), 2), 0, -1): + key = ".".join(parts[:depth]) + target = _COMPONENT_SUBPACKAGE_TARGETS.get(key) + if target is not None: + remainder = ".".join(parts[depth:]) + return f"{target}.{remainder}" if remainder else target + return f".{module}" + + def _get_init_lazy_imports(mod: tuple | ModuleType, new_tree: ast.AST): # retrieve the _SUBMODULES and _SUBMOD_ATTRS from an init file if present. sub_mods: set[str] | None = getattr(mod, "_SUBMODULES", None) @@ -1265,19 +1317,27 @@ def _get_init_lazy_imports(mod: tuple | ModuleType, new_tree: ast.AST): for imported in attrs } # construct the import statement and handle special cases for aliases - sub_mod_attrs_imports = [ - f"from .{module} import " - + ( - ( + for imported, module in flattened_sub_mod_attrs.items(): + # For "components": ["el", "radix", ...], resolve each attr to its package. + if ( + module == "components" + and isinstance(imported, str) + and imported in _COMPONENT_SUBPACKAGE_TARGETS + ): + target = _COMPONENT_SUBPACKAGE_TARGETS[imported] + sub_mod_attrs_imports.append(f"import {target} as {imported}") + continue + + rewritten = _rewrite_component_import(module) + if isinstance(imported, tuple): + suffix = ( (imported[0] + " as " + imported[1]) if imported[0] != imported[1] else imported[0] ) - if isinstance(imported, tuple) - else imported - ) - for imported, module in flattened_sub_mod_attrs.items() - ] + else: + suffix = imported + sub_mod_attrs_imports.append(f"from {rewritten} import {suffix}") sub_mod_attrs_imports.append("") if extra_mappings: diff --git a/reflex/utils/types.py b/reflex/utils/types.py index b375157d556..0005f3fb7fd 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -34,13 +34,13 @@ from typing import get_origin as get_origin_og from typing import get_type_hints as get_type_hints_og +from reflex_components_core.core.breakpoints import Breakpoints from typing_extensions import Self as Self from typing_extensions import override as override import reflex from reflex import constants from reflex.base import Base -from reflex.components.core.breakpoints import Breakpoints from reflex.utils import console # Potential GenericAlias types for isinstance checks. diff --git a/reflex/vars/object.py b/reflex/vars/object.py index 1ecb09394ba..53eb37ae700 100644 --- a/reflex/vars/object.py +++ b/reflex/vars/object.py @@ -251,7 +251,7 @@ def get(self, key: Var | Any, default: Var | Any | None = None) -> Var: Returns: The item from the object. """ - from reflex.components.core.cond import cond + from reflex_components_core.core.cond import cond if default is None: default = Var.create(None) diff --git a/scripts/make_pyi.py b/scripts/make_pyi.py index f4121d755be..6ee5d0c125c 100644 --- a/scripts/make_pyi.py +++ b/scripts/make_pyi.py @@ -12,7 +12,24 @@ LAST_RUN_COMMIT_SHA_FILE = Path(".pyi_generator_last_run").resolve() GENERATOR_FILE = Path(__file__).resolve() GENERATOR_DIFF_FILE = Path(".pyi_generator_diff").resolve() -DEFAULT_TARGETS = ["reflex/components", "reflex/experimental", "reflex/__init__.py"] +DEFAULT_TARGETS = [ + "reflex/components", + "reflex/experimental", + "reflex/__init__.py", + "packages/reflex-components-code/src/reflex_components_code", + "packages/reflex-components-core/src/reflex_components_core", + "packages/reflex-components-dataeditor/src/reflex_components_dataeditor", + "packages/reflex-components-gridjs/src/reflex_components_gridjs", + "packages/reflex-components-lucide/src/reflex_components_lucide", + "packages/reflex-components-markdown/src/reflex_components_markdown", + "packages/reflex-components-moment/src/reflex_components_moment", + "packages/reflex-components-plotly/src/reflex_components_plotly", + "packages/reflex-components-radix/src/reflex_components_radix", + "packages/reflex-components-react-player/src/reflex_components_react_player", + "packages/reflex-components-react-router/src/reflex_components_react_router", + "packages/reflex-components-recharts/src/reflex_components_recharts", + "packages/reflex-components-sonner/src/reflex_components_sonner", +] def _git_diff(args: list[str]) -> str: diff --git a/tests/integration/init-test/in_docker_test_script.sh b/tests/integration/init-test/in_docker_test_script.sh index f17fc3e95fd..78589fae46c 100755 --- a/tests/integration/init-test/in_docker_test_script.sh +++ b/tests/integration/init-test/in_docker_test_script.sh @@ -24,14 +24,15 @@ function do_export () { } echo "Preparing test project dir" -python3 -m venv ~/venv -source ~/venv/bin/activate -pip install -U pip +curl -LsSf https://astral.sh/uv/install.sh | sh +source "$HOME/.local/bin/env" echo "Installing reflex from local repo code" cp -r /reflex-repo ~/reflex-repo -pip install ~/reflex-repo -pip install psutil +uv venv ~/venv +source ~/venv/bin/activate +uv pip install ~/reflex-repo +uv pip install psutil redis-server & diff --git a/tests/integration/test_extra_overlay_function.py b/tests/integration/test_extra_overlay_function.py index 95dcae3e2b4..e766360f8ba 100644 --- a/tests/integration/test_extra_overlay_function.py +++ b/tests/integration/test_extra_overlay_function.py @@ -25,7 +25,7 @@ def index(): app = rx.App() rx.config.get_config().extra_overlay_function = ( - "reflex.components.radix.themes.components.button" + "reflex_components_radix.themes.components.button" ) app.add_page(index) diff --git a/tests/integration/test_icon.py b/tests/integration/test_icon.py index 43313532a62..bcc59f026ef 100644 --- a/tests/integration/test_icon.py +++ b/tests/integration/test_icon.py @@ -3,15 +3,16 @@ from collections.abc import Generator import pytest +from reflex_components_lucide.icon import LUCIDE_ICON_LIST from selenium.webdriver.common.by import By -from reflex.components.lucide.icon import LUCIDE_ICON_LIST from reflex.testing import AppHarness, WebDriver def Icons(): + from reflex_components_lucide.icon import LUCIDE_ICON_LIST + import reflex as rx - from reflex.components.lucide.icon import LUCIDE_ICON_LIST app = rx.App() diff --git a/tests/units/compiler/test_compiler.py b/tests/units/compiler/test_compiler.py index 1a0f87b89a3..2cac70f49da 100644 --- a/tests/units/compiler/test_compiler.py +++ b/tests/units/compiler/test_compiler.py @@ -4,11 +4,11 @@ import pytest from pytest_mock import MockerFixture +from reflex_components_core.base import document +from reflex_components_core.el.elements.metadata import Link from reflex import constants from reflex.compiler import compiler, utils -from reflex.components.base import document -from reflex.components.el.elements.metadata import Link from reflex.constants.compiler import PageNames from reflex.utils.imports import ImportVar, ParsedImportDict from reflex.vars.base import Var diff --git a/tests/units/components/base/test_bare.py b/tests/units/components/base/test_bare.py index d36813badf2..91e3c58fcf4 100644 --- a/tests/units/components/base/test_bare.py +++ b/tests/units/components/base/test_bare.py @@ -1,6 +1,6 @@ import pytest +from reflex_components_core.base.bare import Bare -from reflex.components.base.bare import Bare from reflex.vars.base import Var STATE_VAR = Var(_js_expr="default_state.name") diff --git a/tests/units/components/base/test_link.py b/tests/units/components/base/test_link.py index 3c1260e1293..9c78f4350f6 100644 --- a/tests/units/components/base/test_link.py +++ b/tests/units/components/base/test_link.py @@ -1,4 +1,4 @@ -from reflex.components.base.link import RawLink, ScriptTag +from reflex_components_core.base.link import RawLink, ScriptTag def test_raw_link(): diff --git a/tests/units/components/base/test_script.py b/tests/units/components/base/test_script.py index a279dfefc1e..0f25b8d862d 100644 --- a/tests/units/components/base/test_script.py +++ b/tests/units/components/base/test_script.py @@ -1,8 +1,7 @@ """Test that element script renders correctly.""" import pytest - -from reflex.components.base.script import Script +from reflex_components_core.base.script import Script def test_script_inline(): diff --git a/tests/units/components/core/test_banner.py b/tests/units/components/core/test_banner.py index 78646447d6d..04987db5311 100644 --- a/tests/units/components/core/test_banner.py +++ b/tests/units/components/core/test_banner.py @@ -1,11 +1,11 @@ -from reflex.components.core.banner import ( +from reflex_components_core.core.banner import ( ConnectionBanner, ConnectionModal, ConnectionPulser, WebsocketTargetURL, ) -from reflex.components.radix.themes.base import RadixThemesComponent -from reflex.components.radix.themes.typography.text import Text +from reflex_components_radix.themes.base import RadixThemesComponent +from reflex_components_radix.themes.typography.text import Text def test_websocket_target_url(): diff --git a/tests/units/components/core/test_colors.py b/tests/units/components/core/test_colors.py index aaa82959c92..5e3f621ec61 100644 --- a/tests/units/components/core/test_colors.py +++ b/tests/units/components/core/test_colors.py @@ -1,7 +1,7 @@ import pytest +from reflex_components_code.code import CodeBlock import reflex as rx -from reflex.components.datadisplay.code import CodeBlock from reflex.constants.colors import Color from reflex.constants.state import FIELD_MARKER from reflex.vars.base import LiteralVar diff --git a/tests/units/components/core/test_cond.py b/tests/units/components/core/test_cond.py index 0e1df51d067..9175ded0974 100644 --- a/tests/units/components/core/test_cond.py +++ b/tests/units/components/core/test_cond.py @@ -2,10 +2,10 @@ from typing import Any import pytest +from reflex_components_core.base.fragment import Fragment +from reflex_components_core.core.cond import Cond, cond +from reflex_components_radix.themes.typography.text import Text -from reflex.components.base.fragment import Fragment -from reflex.components.core.cond import Cond, cond -from reflex.components.radix.themes.typography.text import Text from reflex.constants.state import FIELD_MARKER from reflex.state import BaseState from reflex.utils.format import format_state_name diff --git a/tests/units/components/core/test_debounce.py b/tests/units/components/core/test_debounce.py index 43cca255c70..44aacbaf122 100644 --- a/tests/units/components/core/test_debounce.py +++ b/tests/units/components/core/test_debounce.py @@ -1,9 +1,9 @@ """Test that DebounceInput collapses nested forms.""" import pytest +from reflex_components_core.core.debounce import DEFAULT_DEBOUNCE_TIMEOUT import reflex as rx -from reflex.components.core.debounce import DEFAULT_DEBOUNCE_TIMEOUT from reflex.state import BaseState from reflex.vars.base import LiteralVar, Var diff --git a/tests/units/components/core/test_foreach.py b/tests/units/components/core/test_foreach.py index 01b84bdaef6..ee64712cc0b 100644 --- a/tests/units/components/core/test_foreach.py +++ b/tests/units/components/core/test_foreach.py @@ -1,17 +1,17 @@ import pytest - -import reflex as rx -from reflex import el -from reflex.base import Base -from reflex.components.component import Component -from reflex.components.core.foreach import ( +from reflex_components_core.core.foreach import ( Foreach, ForeachRenderError, ForeachVarError, foreach, ) -from reflex.components.radix.themes.layout.box import box -from reflex.components.radix.themes.typography.text import text +from reflex_components_radix.themes.layout.box import box +from reflex_components_radix.themes.typography.text import text + +import reflex as rx +from reflex import el +from reflex.base import Base +from reflex.components.component import Component from reflex.constants.state import FIELD_MARKER from reflex.state import BaseState, ComponentState from reflex.vars.number import NumberVar diff --git a/tests/units/components/core/test_html.py b/tests/units/components/core/test_html.py index 8ab466a4e33..4241c9344ab 100644 --- a/tests/units/components/core/test_html.py +++ b/tests/units/components/core/test_html.py @@ -1,6 +1,6 @@ import pytest +from reflex_components_core.core.html import Html -from reflex.components.core.html import Html from reflex.state import State diff --git a/tests/units/components/core/test_match.py b/tests/units/components/core/test_match.py index 8fd1865fde6..310f9d41cb2 100644 --- a/tests/units/components/core/test_match.py +++ b/tests/units/components/core/test_match.py @@ -1,10 +1,10 @@ import re import pytest +from reflex_components_core.core.match import Match import reflex as rx from reflex.components.component import Component -from reflex.components.core.match import Match from reflex.constants.state import FIELD_MARKER from reflex.state import BaseState from reflex.utils.exceptions import MatchTypeError @@ -141,7 +141,7 @@ def test_match_on_component_without_default(): """Test that matching cases with return values as components returns a Fragment as the default case if not provided. """ - from reflex.components.base.fragment import Fragment + from reflex_components_core.base.fragment import Fragment match_case_tuples = ( (1, rx.text("first value")), @@ -264,7 +264,7 @@ def test_match_case_tuple_elements(match_case): ), ( 'Match cases should have the same return types. Case 3 with return value `jsx(RadixThemesText,{as:"p"},"first value")` ' - "of type is not " + "of type is not " ), ), ], diff --git a/tests/units/components/core/test_responsive.py b/tests/units/components/core/test_responsive.py index 6424ed3c3d9..515af37af9e 100644 --- a/tests/units/components/core/test_responsive.py +++ b/tests/units/components/core/test_responsive.py @@ -1,11 +1,11 @@ -from reflex.components.core.responsive import ( +from reflex_components_core.core.responsive import ( desktop_only, mobile_and_tablet, mobile_only, tablet_and_desktop, tablet_only, ) -from reflex.components.radix.themes.layout.box import Box +from reflex_components_radix.themes.layout.box import Box def test_mobile_only(): diff --git a/tests/units/components/core/test_upload.py b/tests/units/components/core/test_upload.py index 3b03362d6e4..0e1fe3e2137 100644 --- a/tests/units/components/core/test_upload.py +++ b/tests/units/components/core/test_upload.py @@ -1,7 +1,6 @@ from typing import Any -from reflex import event -from reflex.components.core.upload import ( +from reflex_components_core.core.upload import ( StyledUpload, Upload, UploadNamespace, @@ -9,6 +8,8 @@ cancel_upload, get_upload_url, ) + +from reflex import event from reflex.event import EventSpec from reflex.state import State from reflex.vars.base import LiteralVar, Var diff --git a/tests/units/components/datadisplay/test_code.py b/tests/units/components/datadisplay/test_code.py index 85d60cf60b6..5b20c0584ea 100644 --- a/tests/units/components/datadisplay/test_code.py +++ b/tests/units/components/datadisplay/test_code.py @@ -1,6 +1,5 @@ import pytest - -from reflex.components.datadisplay.code import CodeBlock, Theme +from reflex_components_code.code import CodeBlock, Theme @pytest.mark.parametrize( diff --git a/tests/units/components/datadisplay/test_dataeditor.py b/tests/units/components/datadisplay/test_dataeditor.py index 6b0d3ac6103..301149710ae 100644 --- a/tests/units/components/datadisplay/test_dataeditor.py +++ b/tests/units/components/datadisplay/test_dataeditor.py @@ -1,4 +1,4 @@ -from reflex.components.datadisplay.dataeditor import DataEditor +from reflex_components_dataeditor.dataeditor import DataEditor def test_dataeditor(): diff --git a/tests/units/components/datadisplay/test_datatable.py b/tests/units/components/datadisplay/test_datatable.py index 8142870e465..de991618465 100644 --- a/tests/units/components/datadisplay/test_datatable.py +++ b/tests/units/components/datadisplay/test_datatable.py @@ -1,8 +1,8 @@ import pandas as pd import pytest +from reflex_components_gridjs.datatable import DataTable import reflex as rx -from reflex.components.gridjs.datatable import DataTable from reflex.constants.state import FIELD_MARKER from reflex.utils import types from reflex.utils.exceptions import UntypedComputedVarError diff --git a/tests/units/components/datadisplay/test_shiki_code.py b/tests/units/components/datadisplay/test_shiki_code.py index 05553815409..5b33be816f7 100644 --- a/tests/units/components/datadisplay/test_shiki_code.py +++ b/tests/units/components/datadisplay/test_shiki_code.py @@ -1,14 +1,14 @@ import pytest - -from reflex.components.datadisplay.shiki_code_block import ( +from reflex_components_code.shiki_code_block import ( ShikiBaseTransformers, ShikiCodeBlock, ShikiHighLevelCodeBlock, ShikiJsTransformer, ) -from reflex.components.el.elements.forms import Button -from reflex.components.lucide.icon import Icon -from reflex.components.radix.themes.layout.box import Box +from reflex_components_core.el.elements.forms import Button +from reflex_components_lucide.icon import Icon +from reflex_components_radix.themes.layout.box import Box + from reflex.style import Style from reflex.vars import Var from reflex.vars.base import LiteralVar diff --git a/tests/units/components/el/test_svg.py b/tests/units/components/el/test_svg.py index d7d46898508..951b4a3afaa 100644 --- a/tests/units/components/el/test_svg.py +++ b/tests/units/components/el/test_svg.py @@ -1,4 +1,4 @@ -from reflex.components.el.elements.media import ( +from reflex_components_core.el.elements.media import ( Circle, Defs, Ellipse, diff --git a/tests/units/components/forms/test_form.py b/tests/units/components/forms/test_form.py index 6b3d4a06001..ab80d2c9147 100644 --- a/tests/units/components/forms/test_form.py +++ b/tests/units/components/forms/test_form.py @@ -1,4 +1,5 @@ -from reflex.components.radix.primitives.form import Form +from reflex_components_radix.primitives.form import Form + from reflex.event import EventChain, prevent_default from reflex.vars.base import Var diff --git a/tests/units/components/graphing/test_recharts.py b/tests/units/components/graphing/test_recharts.py index c1b986dfda5..3e4268eb891 100644 --- a/tests/units/components/graphing/test_recharts.py +++ b/tests/units/components/graphing/test_recharts.py @@ -1,4 +1,4 @@ -from reflex.components.recharts.charts import ( +from reflex_components_recharts.charts import ( AreaChart, BarChart, LineChart, @@ -7,7 +7,7 @@ RadialBarChart, ScatterChart, ) -from reflex.components.recharts.general import ResponsiveContainer +from reflex_components_recharts.general import ResponsiveContainer def test_area_chart(): diff --git a/tests/units/components/lucide/test_icon.py b/tests/units/components/lucide/test_icon.py index 0f183abc7ae..71d5ab32a63 100644 --- a/tests/units/components/lucide/test_icon.py +++ b/tests/units/components/lucide/test_icon.py @@ -1,10 +1,10 @@ import pytest - -from reflex.components.lucide.icon import ( +from reflex_components_lucide.icon import ( LUCIDE_ICON_LIST, LUCIDE_ICON_MAPPING_OVERRIDE, Icon, ) + from reflex.utils import format diff --git a/tests/units/components/markdown/test_markdown.py b/tests/units/components/markdown/test_markdown.py index b91084a09fa..4f766c42aaa 100644 --- a/tests/units/components/markdown/test_markdown.py +++ b/tests/units/components/markdown/test_markdown.py @@ -1,11 +1,11 @@ import pytest +from reflex_components_code.code import CodeBlock +from reflex_components_code.shiki_code_block import ShikiHighLevelCodeBlock +from reflex_components_markdown.markdown import Markdown, MarkdownComponentMap +from reflex_components_radix.themes.layout.box import Box +from reflex_components_radix.themes.typography.heading import Heading from reflex.components.component import Component, memo -from reflex.components.datadisplay.code import CodeBlock -from reflex.components.datadisplay.shiki_code_block import ShikiHighLevelCodeBlock -from reflex.components.markdown.markdown import Markdown, MarkdownComponentMap -from reflex.components.radix.themes.layout.box import Box -from reflex.components.radix.themes.typography.heading import Heading from reflex.vars.base import Var diff --git a/tests/units/components/radix/test_icon_button.py b/tests/units/components/radix/test_icon_button.py index 8047cf7b2d9..7314293b970 100644 --- a/tests/units/components/radix/test_icon_button.py +++ b/tests/units/components/radix/test_icon_button.py @@ -1,7 +1,7 @@ import pytest +from reflex_components_lucide.icon import Icon +from reflex_components_radix.themes.components.icon_button import IconButton -from reflex.components.lucide.icon import Icon -from reflex.components.radix.themes.components.icon_button import IconButton from reflex.style import Style from reflex.vars.base import LiteralVar diff --git a/tests/units/components/radix/test_layout.py b/tests/units/components/radix/test_layout.py index 73fcde2a87a..f84b27fda2f 100644 --- a/tests/units/components/radix/test_layout.py +++ b/tests/units/components/radix/test_layout.py @@ -1,4 +1,4 @@ -from reflex.components.radix.themes.layout.base import LayoutComponent +from reflex_components_radix.themes.layout.base import LayoutComponent def test_layout_component(): diff --git a/tests/units/components/recharts/test_cartesian.py b/tests/units/components/recharts/test_cartesian.py index 3337427bd13..078455f4411 100644 --- a/tests/units/components/recharts/test_cartesian.py +++ b/tests/units/components/recharts/test_cartesian.py @@ -1,4 +1,4 @@ -from reflex.components.recharts import ( +from reflex_components_recharts import ( Area, Bar, Brush, diff --git a/tests/units/components/recharts/test_polar.py b/tests/units/components/recharts/test_polar.py index 4e4af0f498c..27c3561a44c 100644 --- a/tests/units/components/recharts/test_polar.py +++ b/tests/units/components/recharts/test_polar.py @@ -1,4 +1,4 @@ -from reflex.components.recharts import ( +from reflex_components_recharts import ( Pie, PolarAngleAxis, PolarGrid, diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index 959c81c34bb..550c2dc95e4 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -2,12 +2,13 @@ from typing import Any, ClassVar import pytest +from reflex_components_core.base.bare import Bare +from reflex_components_core.base.fragment import Fragment +from reflex_components_radix.themes.layout.box import Box import reflex as rx from reflex.base import Base from reflex.compiler.utils import compile_custom_component -from reflex.components.base.bare import Bare -from reflex.components.base.fragment import Fragment from reflex.components.component import ( CUSTOM_COMPONENTS, Component, @@ -15,7 +16,6 @@ StatefulComponent, custom_component, ) -from reflex.components.radix.themes.layout.box import Box from reflex.constants import EventTriggers from reflex.constants.state import FIELD_MARKER from reflex.event import ( @@ -861,7 +861,7 @@ def my_component(width: Var[int], color: Var[str]): color=color, ) - from reflex.components.radix.themes.typography.text import Text + from reflex_components_radix.themes.typography.text import Text ccomponent = my_component( rx.text("child"), width=LiteralVar.create(1), color=LiteralVar.create("red") diff --git a/tests/units/components/test_component_state.py b/tests/units/components/test_component_state.py index 1b62e35c81d..333f9f5603d 100644 --- a/tests/units/components/test_component_state.py +++ b/tests/units/components/test_component_state.py @@ -1,9 +1,9 @@ """Ensure that Components returned by ComponentState.create have independent State classes.""" import pytest +from reflex_components_core.base.bare import Bare import reflex as rx -from reflex.components.base.bare import Bare from reflex.utils.exceptions import ReflexRuntimeError diff --git a/tests/units/components/typography/test_markdown.py b/tests/units/components/typography/test_markdown.py index d104d4c3812..2ddc591252b 100644 --- a/tests/units/components/typography/test_markdown.py +++ b/tests/units/components/typography/test_markdown.py @@ -1,7 +1,7 @@ import pytest +from reflex_components_markdown.markdown import Markdown import reflex as rx -from reflex.components.markdown.markdown import Markdown @pytest.mark.parametrize( diff --git a/tests/units/docgen/__init__.py b/tests/units/docgen/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/units/test_docgen.py b/tests/units/docgen/test_class_and_component.py similarity index 100% rename from tests/units/test_docgen.py rename to tests/units/docgen/test_class_and_component.py diff --git a/tests/units/docgen/test_markdown.py b/tests/units/docgen/test_markdown.py new file mode 100644 index 00000000000..0f589406d78 --- /dev/null +++ b/tests/units/docgen/test_markdown.py @@ -0,0 +1,735 @@ +"""Tests for reflex-docgen markdown parsing.""" + +from pathlib import Path + +import pytest +from reflex_docgen.markdown import ( + BoldSpan, + CodeSpan, + ComponentPreview, + FrontMatter, + ImageSpan, + ItalicSpan, + LinkSpan, + StrikethroughSpan, + TextBlock, + TextSpan, + parse_document, +) + +_DOCS_DIR = Path(__file__).resolve().parents[3] / "docs" + + +def test_no_frontmatter(): + """Parsing a document without frontmatter returns None.""" + doc = parse_document("# Hello\n\nWorld\n") + assert doc.frontmatter is None + + +def test_basic_frontmatter(): + """A simple YAML frontmatter block is extracted.""" + source = "---\ntitle: Test\n---\n# Hello\n" + doc = parse_document(source) + assert doc.frontmatter is not None + assert doc.frontmatter.title == "Test" + + +def test_multiline_frontmatter(): + """Multi-line YAML frontmatter is preserved.""" + source = "---\ncomponents:\n - rx.button\n\nButton: |\n lambda **props: rx.button(**props)\n---\n# Button\n" + doc = parse_document(source) + assert doc.frontmatter is not None + assert doc.frontmatter.components == ("rx.button",) + assert len(doc.frontmatter.component_previews) == 1 + assert doc.frontmatter.component_previews[0].name == "Button" + + +def test_frontmatter_not_in_blocks(): + """Frontmatter should not appear in the blocks list.""" + source = "---\ntitle: Test\n---\n# Hello\n" + doc = parse_document(source) + assert not any(isinstance(b, FrontMatter) for b in doc.blocks) + + +def test_empty_frontmatter(): + """Empty frontmatter (no content between ---) is not recognized.""" + doc = parse_document("---\n---\n# Hello\n") + assert doc.frontmatter is None + + +def test_only_low_level_list_true(): + """only_low_level as a YAML list with True is parsed.""" + source = "---\nonly_low_level:\n - True\n---\n# Dialog\n" + fm = parse_document(source).frontmatter + assert fm is not None + assert fm.only_low_level is True + + +def test_only_low_level_scalar(): + """only_low_level as a scalar boolean is parsed.""" + source = "---\nonly_low_level: true\n---\n# Dialog\n" + fm = parse_document(source).frontmatter + assert fm is not None + assert fm.only_low_level is True + + +def test_only_low_level_default_false(): + """only_low_level defaults to False when absent.""" + source = "---\ncomponents:\n - rx.box\n---\n# Box\n" + fm = parse_document(source).frontmatter + assert fm is not None + assert fm.only_low_level is False + + +def test_component_previews(): + """A component preview lambda is extracted from frontmatter.""" + source = '---\ncomponents:\n - rx.button\n\nButton: |\n lambda **props: rx.button("Click me", **props)\n---\n# Button\n' + fm = parse_document(source).frontmatter + assert fm is not None + assert len(fm.component_previews) == 1 + preview = fm.component_previews[0] + assert preview.name == "Button" + assert "rx.button" in preview.source + assert preview.source.startswith("lambda") + + +def test_multiple_previews(): + """Multiple component preview lambdas are extracted.""" + source = "---\ncomponents:\n - rx.input\n\nInput: |\n lambda **props: rx.input(**props)\n\nInputSlot: |\n lambda **props: rx.input(rx.input.slot(**props))\n---\n# Input\n" + fm = parse_document(source).frontmatter + assert fm is not None + assert len(fm.component_previews) == 2 + names = [p.name for p in fm.component_previews] + assert "Input" in names + assert "InputSlot" in names + + +def test_as_markdown_frontmatter_with_previews(): + """FrontMatter with component previews renders correctly.""" + fm = FrontMatter( + components=("rx.button",), + only_low_level=False, + title=None, + component_previews=( + ComponentPreview( + name="Button", + source='lambda **props: rx.button("Click", **props)', + ), + ), + ) + md = fm.as_markdown() + assert md.startswith("---\n") + assert md.endswith("\n---") + assert "rx.button" in md + assert "Button:" in md + + +def test_as_markdown_frontmatter_with_only_low_level(): + """FrontMatter with only_low_level renders the field.""" + fm = FrontMatter( + components=(), + only_low_level=True, + title=None, + component_previews=(), + ) + assert "only_low_level" in fm.as_markdown() + + +def test_h1(): + """A level-1 heading is parsed correctly.""" + doc = parse_document("# Title\n") + assert len(doc.headings) == 1 + assert doc.headings[0].level == 1 + assert doc.headings[0].children == (TextSpan(text="Title"),) + + +def test_multiple_heading_levels(): + """Headings at different levels are all captured.""" + source = "# H1\n\n## H2\n\n### H3\n" + doc = parse_document(source) + assert len(doc.headings) == 3 + assert [h.level for h in doc.headings] == [1, 2, 3] + assert [h.children for h in doc.headings] == [ + (TextSpan(text="H1"),), + (TextSpan(text="H2"),), + (TextSpan(text="H3"),), + ] + + +def test_heading_with_inline_code(): + """Inline code in headings is preserved as a CodeSpan.""" + doc = parse_document("# The `rx.button` Component\n") + children = doc.headings[0].children + assert children == ( + TextSpan(text="The "), + CodeSpan(code="rx.button"), + TextSpan(text=" Component"), + ) + + +def test_plain_code_block(): + """A fenced code block with only a language is parsed.""" + source = "```python\nx = 1\n```\n" + doc = parse_document(source) + assert len(doc.code_blocks) == 1 + cb = doc.code_blocks[0] + assert cb.language == "python" + assert cb.flags == () + assert cb.content == "x = 1" + + +def test_code_block_with_flags(): + """A fenced code block with demo and exec flags is parsed.""" + source = "```python demo exec\nclass Foo:\n pass\n```\n" + doc = parse_document(source) + assert len(doc.code_blocks) == 1 + cb = doc.code_blocks[0] + assert cb.language == "python" + assert cb.flags == ("demo", "exec") + + +def test_code_block_demo_only(): + """A fenced code block with only the demo flag is parsed.""" + source = "```python demo\nrx.button('Click')\n```\n" + doc = parse_document(source) + cb = doc.code_blocks[0] + assert cb.language == "python" + assert cb.flags == ("demo",) + + +def test_code_block_exec_only(): + """A fenced code block with only the exec flag is parsed.""" + source = "```python exec\nimport reflex as rx\n```\n" + doc = parse_document(source) + cb = doc.code_blocks[0] + assert cb.language == "python" + assert cb.flags == ("exec",) + + +def test_code_block_eval(): + """A fenced code block with the eval flag is parsed.""" + source = "```python eval\nrx.text('hello')\n```\n" + doc = parse_document(source) + cb = doc.code_blocks[0] + assert cb.language == "python" + assert cb.flags == ("eval",) + + +def test_code_block_no_language(): + """A fenced code block without a language is parsed.""" + source = "```\nplain text\n```\n" + doc = parse_document(source) + assert len(doc.code_blocks) == 1 + cb = doc.code_blocks[0] + assert cb.language is None + assert cb.flags == () + + +def test_directive_alert(): + """An md alert directive is parsed as a DirectiveBlock.""" + source = "```md alert\n# Warning Title\n\nThis is the body.\n```\n" + doc = parse_document(source) + assert len(doc.directives) == 1 + d = doc.directives[0] + assert d.name == "alert" + assert d.args == () + + +def test_directive_alert_with_variant(): + """An md alert with a variant like 'info' preserves the variant as an arg.""" + source = "```md alert info\n# Note\nSome info.\n```\n" + doc = parse_document(source) + d = doc.directives[0] + assert d.name == "alert" + assert d.args == ("info",) + + +def test_directive_alert_warning(): + """An md alert warning directive preserves the warning arg.""" + source = "```md alert warning\nDo not do this.\n```\n" + doc = parse_document(source) + d = doc.directives[0] + assert d.name == "alert" + assert d.args == ("warning",) + + +def test_directive_video(): + """An md video directive captures the URL as an arg.""" + source = "```md video https://youtube.com/embed/abc123\n```\n" + doc = parse_document(source) + d = doc.directives[0] + assert d.name == "video" + assert d.args == ("https://youtube.com/embed/abc123",) + + +def test_directive_definition(): + """An md definition directive is parsed.""" + source = "```md definition\nSome definition content.\n```\n" + doc = parse_document(source) + d = doc.directives[0] + assert d.name == "definition" + assert d.args == () + assert d.content == "Some definition content." + + +def test_directive_section(): + """An md section directive is parsed.""" + source = "```md section\nSection content here.\n```\n" + doc = parse_document(source) + d = doc.directives[0] + assert d.name == "section" + assert d.args == () + + +def test_directive_not_in_code_blocks(): + """Directive blocks should not appear in the code_blocks list.""" + source = "```md alert\nBody\n```\n" + doc = parse_document(source) + assert len(doc.code_blocks) == 0 + assert len(doc.directives) == 1 + + +def test_simple_paragraph(): + """A plain paragraph is captured as a TextBlock with a TextSpan.""" + doc = parse_document("Hello world.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert len(text_blocks) == 1 + assert text_blocks[0].children == (TextSpan(text="Hello world."),) + + +def test_paragraph_with_inline_code(): + """Inline code in paragraphs is preserved as a CodeSpan.""" + doc = parse_document("Use `rx.button` for buttons.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="Use "), + CodeSpan(code="rx.button"), + TextSpan(text=" for buttons."), + ) + + +def test_bold_text(): + """Bold text is parsed into a BoldSpan.""" + doc = parse_document("This is **bold** text.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="This is "), + BoldSpan(children=(TextSpan(text="bold"),)), + TextSpan(text=" text."), + ) + + +def test_italic_text(): + """Italic text is parsed into an ItalicSpan.""" + doc = parse_document("This is *italic* text.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="This is "), + ItalicSpan(children=(TextSpan(text="italic"),)), + TextSpan(text=" text."), + ) + + +def test_strikethrough_text(): + """Strikethrough text is parsed into a StrikethroughSpan.""" + doc = parse_document("This is ~~struck~~ text.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="This is "), + StrikethroughSpan(children=(TextSpan(text="struck"),)), + TextSpan(text=" text."), + ) + + +def test_link(): + """Links are parsed into LinkSpans.""" + doc = parse_document("Click [here](http://example.com) now.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="Click "), + LinkSpan(children=(TextSpan(text="here"),), target="http://example.com"), + TextSpan(text=" now."), + ) + + +def test_image(): + """Images are parsed into ImageSpans.""" + doc = parse_document("See ![alt text](image.png) here.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="See "), + ImageSpan(children=(TextSpan(text="alt text"),), src="image.png"), + TextSpan(text=" here."), + ) + + +def test_nested_spans(): + """Bold containing code is parsed as nested spans.""" + doc = parse_document("Use **`rx.button`** here.\n") + text_blocks = [b for b in doc.blocks if isinstance(b, TextBlock)] + assert text_blocks[0].children == ( + TextSpan(text="Use "), + BoldSpan(children=(CodeSpan(code="rx.button"),)), + TextSpan(text=" here."), + ) + + +def test_mixed_document(): + """A document with frontmatter, headings, code, and directives is fully parsed.""" + source = ( + "---\ntitle: Test\n---\n" + "# Title\n\n" + "Some text.\n\n" + "```python demo\ncode()\n```\n\n" + "```md alert\nAlert body.\n```\n\n" + "## Section\n" + ) + doc = parse_document(source) + assert doc.frontmatter is not None + assert len(doc.headings) == 2 + assert len(doc.code_blocks) == 1 + assert len(doc.directives) == 1 + + +def test_empty_document(): + """An empty string produces an empty Document.""" + doc = parse_document("") + assert doc.frontmatter is None + assert doc.blocks == () + + +def test_realistic_doc_structure(): + """Verify parsing of a realistic Reflex doc structure.""" + source = """\ +--- +components: + - rx.button +--- + +```python exec +import reflex as rx +``` + +# Button + +Buttons trigger events. + +```python demo exec +class State(rx.State): + count: int = 0 + +def button_demo(): + return rx.button("Click", on_click=State.increment) +``` + +```md alert info +# Important + +Use `on_click` for click events. +``` + +```md video https://youtube.com/embed/abc123 + +``` + +## Variants +""" + doc = parse_document(source) + assert doc.frontmatter is not None + assert doc.frontmatter.components == ("rx.button",) + assert len(doc.headings) == 2 + assert doc.headings[0].children == (TextSpan(text="Button"),) + assert doc.headings[1].children == (TextSpan(text="Variants"),) + assert len(doc.code_blocks) == 2 + assert doc.code_blocks[0].flags == ("exec",) + assert doc.code_blocks[1].flags == ("demo", "exec") + assert len(doc.directives) == 2 + assert doc.directives[0].name == "alert" + assert doc.directives[0].args == ("info",) + assert doc.directives[1].name == "video" + + +_ALL_MD_FILES = sorted(_DOCS_DIR.rglob("*.md")) + + +@pytest.mark.parametrize( + "md_file", _ALL_MD_FILES, ids=lambda p: str(p.relative_to(_DOCS_DIR)) +) +def test_parse_all_doc_files(md_file: Path): + """Every markdown file in docs/ should parse without errors.""" + source = md_file.read_text(encoding="utf-8") + doc = parse_document(source) + # Sanity check: a non-empty file should produce at least one block. + if source.strip(): + assert len(doc.blocks) > 0 + # Verify as_markdown doesn't crash on any doc file. + doc.as_markdown() + + +# --------------------------------------------------------------------------- +# as_markdown round-trip tests +# --------------------------------------------------------------------------- + + +def test_as_markdown_text_span(): + """TextSpan renders back to plain text.""" + assert TextSpan(text="hello").as_markdown() == "hello" + + +def test_as_markdown_code_span(): + """CodeSpan renders back with backticks.""" + assert CodeSpan(code="rx.button").as_markdown() == "`rx.button`" + + +def test_as_markdown_bold_span(): + """BoldSpan renders back with double asterisks.""" + span = BoldSpan(children=(TextSpan(text="bold"),)) + assert span.as_markdown() == "**bold**" + + +def test_as_markdown_italic_span(): + """ItalicSpan renders back with single asterisks.""" + span = ItalicSpan(children=(TextSpan(text="italic"),)) + assert span.as_markdown() == "*italic*" + + +def test_as_markdown_strikethrough_span(): + """StrikethroughSpan renders back with tildes.""" + span = StrikethroughSpan(children=(TextSpan(text="struck"),)) + assert span.as_markdown() == "~~struck~~" + + +def test_as_markdown_link_span(): + """LinkSpan renders back as a markdown link.""" + span = LinkSpan(children=(TextSpan(text="click"),), target="http://x.com") + assert span.as_markdown() == "[click](http://x.com)" + + +def test_as_markdown_image_span(): + """ImageSpan renders back as a markdown image.""" + span = ImageSpan(children=(TextSpan(text="alt"),), src="img.png") + assert span.as_markdown() == "![alt](img.png)" + + +def test_as_markdown_line_break_soft(): + """Soft LineBreakSpan renders as a newline.""" + from reflex_docgen.markdown import LineBreakSpan + + assert LineBreakSpan(soft=True).as_markdown() == "\n" + + +def test_as_markdown_line_break_hard(): + """Hard LineBreakSpan renders as two spaces + newline.""" + from reflex_docgen.markdown import LineBreakSpan + + assert LineBreakSpan(soft=False).as_markdown() == " \n" + + +def test_as_markdown_nested_spans(): + """Nested spans render correctly.""" + span = BoldSpan(children=(CodeSpan(code="x"), TextSpan(text=" = 1"))) + assert span.as_markdown() == "**`x` = 1**" + + +def test_as_markdown_heading(): + """HeadingBlock renders with the correct number of hashes.""" + from reflex_docgen.markdown import HeadingBlock + + h1 = HeadingBlock(level=1, children=(TextSpan(text="Title"),)) + assert h1.as_markdown() == "# Title" + h3 = HeadingBlock(level=3, children=(TextSpan(text="Sub"),)) + assert h3.as_markdown() == "### Sub" + + +def test_as_markdown_heading_with_inline(): + """HeadingBlock with mixed spans renders correctly.""" + from reflex_docgen.markdown import HeadingBlock + + h = HeadingBlock( + level=2, + children=( + TextSpan(text="The "), + CodeSpan(code="rx.button"), + TextSpan(text=" API"), + ), + ) + assert h.as_markdown() == "## The `rx.button` API" + + +def test_as_markdown_text_block(): + """TextBlock renders its children as a paragraph.""" + block = TextBlock( + children=(TextSpan(text="Hello "), BoldSpan(children=(TextSpan(text="world"),))) + ) + assert block.as_markdown() == "Hello **world**" + + +def test_as_markdown_code_block(): + """CodeBlock renders as a fenced code block.""" + from reflex_docgen.markdown import CodeBlock + + cb = CodeBlock(language="python", flags=("demo", "exec"), content="x = 1") + assert cb.as_markdown() == "```python demo exec\nx = 1\n```" + + +def test_as_markdown_code_block_no_language(): + """CodeBlock without language renders with empty info string.""" + from reflex_docgen.markdown import CodeBlock + + cb = CodeBlock(language=None, flags=(), content="plain") + assert cb.as_markdown() == "```\nplain\n```" + + +def test_as_markdown_directive(): + """DirectiveBlock renders as a fenced md block.""" + from reflex_docgen.markdown import DirectiveBlock + + d = DirectiveBlock(name="alert", args=("info",), content="Be careful.") + assert d.as_markdown() == "```md alert info\nBe careful.\n```" + + +def test_as_markdown_list_unordered(): + """Unordered ListBlock renders with dashes.""" + from reflex_docgen.markdown import ListBlock, ListItem + + lb = ListBlock( + ordered=False, + start=None, + items=( + ListItem(children=(TextBlock(children=(TextSpan(text="one"),)),)), + ListItem(children=(TextBlock(children=(TextSpan(text="two"),)),)), + ), + ) + assert lb.as_markdown() == "- one\n- two" + + +def test_as_markdown_list_ordered(): + """Ordered ListBlock renders with numbers.""" + from reflex_docgen.markdown import ListBlock, ListItem + + lb = ListBlock( + ordered=True, + start=1, + items=( + ListItem(children=(TextBlock(children=(TextSpan(text="first"),)),)), + ListItem(children=(TextBlock(children=(TextSpan(text="second"),)),)), + ), + ) + assert lb.as_markdown() == "1. first\n2. second" + + +def test_as_markdown_quote(): + """QuoteBlock renders with > prefix.""" + from reflex_docgen.markdown import QuoteBlock + + q = QuoteBlock(children=(TextBlock(children=(TextSpan(text="wise words"),)),)) + assert q.as_markdown() == "> wise words" + + +def test_as_markdown_table(): + """TableBlock renders as a markdown table.""" + from reflex_docgen.markdown import TableBlock, TableCell, TableRow + + table = TableBlock( + header=TableRow( + cells=( + TableCell(children=(TextSpan(text="Name"),), align=None), + TableCell(children=(TextSpan(text="Value"),), align="right"), + ) + ), + rows=( + TableRow( + cells=( + TableCell(children=(TextSpan(text="a"),), align=None), + TableCell(children=(TextSpan(text="1"),), align="right"), + ) + ), + ), + ) + expected = "| Name | Value |\n| --- | ---: |\n| a | 1 |" + assert table.as_markdown() == expected + + +def test_as_markdown_thematic_break(): + """ThematicBreakBlock renders as ---.""" + from reflex_docgen.markdown import ThematicBreakBlock + + assert ThematicBreakBlock().as_markdown() == "---" + + +def test_as_markdown_frontmatter(): + """FrontMatter renders with --- delimiters.""" + fm = FrontMatter( + components=(), + only_low_level=False, + title="Test", + component_previews=(), + ) + md = fm.as_markdown() + assert md.startswith("---\n") + assert md.endswith("\n---") + assert "title: Test" in md + + +def test_as_markdown_document_roundtrip(): + """Document.as_markdown produces valid markdown that re-parses consistently.""" + source = """\ +--- +title: Test +--- + +# Hello **world** + +Use `rx.button` for [buttons](http://example.com). + +```python demo exec +x = 1 +``` + +```md alert info +# Warning +Be careful. +``` + +- item one +- item **two** + +--- +""" + doc = parse_document(source) + rendered = doc.as_markdown() + doc2 = parse_document(rendered) + # The re-parsed document should produce the same markdown. + assert doc2.as_markdown() == rendered + + +def test_nested_code_block_in_directive(): + """A directive using more backticks can contain inner code fences.""" + source = "````md alert\n# Example\n\n```python\nx = 1\n```\n````\n" + doc = parse_document(source) + assert len(doc.directives) == 1 + d = doc.directives[0] + assert d.name == "alert" + assert "```python" in d.content + assert "x = 1" in d.content + + +def test_nested_code_block_in_code_block(): + """A code block using more backticks can contain inner code fences.""" + source = "````python demo\nrx.markdown(\n '''```python\nx = 1\n```'''\n)\n````\n" + doc = parse_document(source) + assert len(doc.code_blocks) == 1 + cb = doc.code_blocks[0] + assert cb.language == "python" + assert cb.flags == ("demo",) + assert "```python" in cb.content + + +def test_nested_code_block_roundtrip(): + """Nested code blocks survive a parse-render-reparse cycle.""" + source = "````md alert warning\n# Note\n\n```python\nx = 1\n```\n````\n" + doc = parse_document(source) + rendered = doc.as_markdown() + doc2 = parse_document(rendered) + assert len(doc2.directives) == 1 + assert doc2.directives[0].content == doc.directives[0].content diff --git a/tests/units/test_app.py b/tests/units/test_app.py index 25c71c0d17e..e5a75395294 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -15,6 +15,10 @@ import pytest from pytest_mock import MockerFixture +from reflex_components_core.base.bare import Bare +from reflex_components_core.base.fragment import Fragment +from reflex_components_core.core.cond import Cond +from reflex_components_radix.themes.typography.text import Text from starlette.applications import Starlette from starlette.datastructures import FormData, UploadFile from starlette.responses import StreamingResponse @@ -29,10 +33,6 @@ upload, ) from reflex.components import Component -from reflex.components.base.bare import Bare -from reflex.components.base.fragment import Fragment -from reflex.components.core.cond import Cond -from reflex.components.radix.themes.typography.text import Text from reflex.constants.state import FIELD_MARKER from reflex.environment import environment from reflex.event import Event @@ -1842,7 +1842,7 @@ def test_call_app(): def test_app_with_optional_endpoints(): - from reflex.components.core.upload import Upload + from reflex_components_core.core.upload import Upload app = App() Upload.is_used = True diff --git a/tests/units/test_event.py b/tests/units/test_event.py index c413a1f225e..876da35e529 100644 --- a/tests/units/test_event.py +++ b/tests/units/test_event.py @@ -640,7 +640,7 @@ async def handle_old_background(self): def test_event_var_in_rx_cond(): """Test that EventVar and EventChainVar cannot be used in rx.cond().""" - from reflex.components.core.cond import cond as rx_cond + from reflex_components_core.core.cond import cond as rx_cond class S(BaseState): @event diff --git a/tests/units/test_var.py b/tests/units/test_var.py index 402505c0543..32778417a75 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -1896,7 +1896,7 @@ class StateWithVar(rx.State): field: int = 1 mocker.patch( - "reflex.components.base.bare.get_performance_mode", + "reflex_components_core.base.bare.get_performance_mode", return_value=PerformanceMode.RAISE, ) @@ -1906,7 +1906,7 @@ class StateWithVar(rx.State): ) mocker.patch( - "reflex.components.base.bare.get_performance_mode", + "reflex_components_core.base.bare.get_performance_mode", return_value=PerformanceMode.OFF, ) diff --git a/tests/units/utils/test_serializers.py b/tests/units/utils/test_serializers.py index 6c085c4ceef..c3a01bae508 100644 --- a/tests/units/utils/test_serializers.py +++ b/tests/units/utils/test_serializers.py @@ -6,9 +6,9 @@ from typing import Any import pytest +from reflex_components_core.core.colors import Color from reflex.base import Base -from reflex.components.core.colors import Color from reflex.utils import serializers from reflex.utils.format import json_dumps from reflex.vars.base import LiteralVar diff --git a/uv.lock b/uv.lock index 154e46b320c..ce618c353e8 100644 --- a/uv.lock +++ b/uv.lock @@ -14,6 +14,19 @@ resolution-markers = [ [manifest] members = [ "reflex", + "reflex-components-code", + "reflex-components-core", + "reflex-components-dataeditor", + "reflex-components-gridjs", + "reflex-components-lucide", + "reflex-components-markdown", + "reflex-components-moment", + "reflex-components-plotly", + "reflex-components-radix", + "reflex-components-react-player", + "reflex-components-react-router", + "reflex-components-recharts", + "reflex-components-sonner", "reflex-docgen", ] @@ -903,6 +916,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mistletoe" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/ae/d33647e2a26a8899224f36afc5e7b7a670af30f1fd87231e9f07ca19d673/mistletoe-1.5.1.tar.gz", hash = "sha256:c5571ce6ca9cfdc7ce9151c3ae79acb418e067812000907616427197648030a3", size = 111769, upload-time = "2025-12-07T16:19:01.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/60/0980fefdc4d12c18c1bbab9d62852f27aded8839233c7b0a9827aaf395f5/mistletoe-1.5.1-py3-none-any.whl", hash = "sha256:d3e97664798261503f685f6a6281b092628367cf3128fc68a015a993b0c4feb3", size = 55331, upload-time = "2025-12-07T16:18:59.65Z" }, +] + [[package]] name = "narwhals" version = "2.18.0" @@ -2018,7 +2040,6 @@ wheels = [ [[package]] name = "reflex" -version = "0.9.0.dev1" source = { editable = "." } dependencies = [ { name = "alembic" }, @@ -2032,6 +2053,19 @@ dependencies = [ { name = "python-multipart" }, { name = "python-socketio" }, { name = "redis" }, + { name = "reflex-components-code" }, + { name = "reflex-components-core" }, + { name = "reflex-components-dataeditor" }, + { name = "reflex-components-gridjs" }, + { name = "reflex-components-lucide" }, + { name = "reflex-components-markdown" }, + { name = "reflex-components-moment" }, + { name = "reflex-components-plotly" }, + { name = "reflex-components-radix" }, + { name = "reflex-components-react-player" }, + { name = "reflex-components-react-router" }, + { name = "reflex-components-recharts" }, + { name = "reflex-components-sonner" }, { name = "reflex-hosting-cli" }, { name = "rich" }, { name = "sqlmodel" }, @@ -2099,6 +2133,19 @@ requires-dist = [ { name = "python-multipart", specifier = ">=0.0.20,<1.0" }, { name = "python-socketio", specifier = ">=5.12.0,<6.0" }, { name = "redis", specifier = ">=5.2.1,<8.0" }, + { name = "reflex-components-code", editable = "packages/reflex-components-code" }, + { name = "reflex-components-core", editable = "packages/reflex-components-core" }, + { name = "reflex-components-dataeditor", editable = "packages/reflex-components-dataeditor" }, + { name = "reflex-components-gridjs", editable = "packages/reflex-components-gridjs" }, + { name = "reflex-components-lucide", editable = "packages/reflex-components-lucide" }, + { name = "reflex-components-markdown", editable = "packages/reflex-components-markdown" }, + { name = "reflex-components-moment", editable = "packages/reflex-components-moment" }, + { name = "reflex-components-plotly", editable = "packages/reflex-components-plotly" }, + { name = "reflex-components-radix", editable = "packages/reflex-components-radix" }, + { name = "reflex-components-react-player", editable = "packages/reflex-components-react-player" }, + { name = "reflex-components-react-router", editable = "packages/reflex-components-react-router" }, + { name = "reflex-components-recharts", editable = "packages/reflex-components-recharts" }, + { name = "reflex-components-sonner", editable = "packages/reflex-components-sonner" }, { name = "reflex-hosting-cli", specifier = ">=0.1.61" }, { name = "rich", specifier = ">=13,<15" }, { name = "sqlmodel", specifier = ">=0.0.27,<0.1" }, @@ -2144,12 +2191,65 @@ dev = [ { name = "uvicorn" }, ] +[[package]] +name = "reflex-components-code" +source = { editable = "packages/reflex-components-code" } + +[[package]] +name = "reflex-components-core" +source = { editable = "packages/reflex-components-core" } + +[[package]] +name = "reflex-components-dataeditor" +source = { editable = "packages/reflex-components-dataeditor" } + +[[package]] +name = "reflex-components-gridjs" +source = { editable = "packages/reflex-components-gridjs" } + +[[package]] +name = "reflex-components-lucide" +source = { editable = "packages/reflex-components-lucide" } + +[[package]] +name = "reflex-components-markdown" +source = { editable = "packages/reflex-components-markdown" } + +[[package]] +name = "reflex-components-moment" +source = { editable = "packages/reflex-components-moment" } + +[[package]] +name = "reflex-components-plotly" +source = { editable = "packages/reflex-components-plotly" } + +[[package]] +name = "reflex-components-radix" +source = { editable = "packages/reflex-components-radix" } + +[[package]] +name = "reflex-components-react-player" +source = { editable = "packages/reflex-components-react-player" } + +[[package]] +name = "reflex-components-react-router" +source = { editable = "packages/reflex-components-react-router" } + +[[package]] +name = "reflex-components-recharts" +source = { editable = "packages/reflex-components-recharts" } + +[[package]] +name = "reflex-components-sonner" +source = { editable = "packages/reflex-components-sonner" } + [[package]] name = "reflex-docgen" -version = "0.0.1" source = { editable = "packages/reflex-docgen" } dependencies = [ { name = "griffelib" }, + { name = "mistletoe" }, + { name = "pyyaml" }, { name = "reflex" }, { name = "typing-extensions" }, { name = "typing-inspection" }, @@ -2158,6 +2258,8 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "griffelib", specifier = ">=2.0.1" }, + { name = "mistletoe", specifier = ">=1.4.0" }, + { name = "pyyaml", specifier = ">=6.0" }, { name = "reflex", editable = "." }, { name = "typing-extensions" }, { name = "typing-inspection", specifier = ">=0.4.2" },