diff --git a/docs/api-reference/browser_storage.md b/docs/api-reference/browser_storage.md index f90436affca..59834ea3ed3 100644 --- a/docs/api-reference/browser_storage.md +++ b/docs/api-reference/browser_storage.md @@ -25,6 +25,15 @@ class CookieState(rx.State): c6: str = rx.Cookie(name="c6-custom-name") ``` +```md alert info +# The default value is the first positional argument. + +`rx.Cookie`, `rx.LocalStorage`, and `rx.SessionStorage` all take the default +value as their first positional argument, e.g. `rx.LocalStorage("light")`. It +cannot be passed as a keyword argument — the keyword arguments (`name`, +`max_age`, `sync`, etc.) only configure the storage behavior. +``` + ```md alert warning # **The default value of a Cookie is never set in the browser!** diff --git a/docs/api-reference/event_triggers.md b/docs/api-reference/event_triggers.md index 6c8c3a53978..4fc0ad9ed78 100644 --- a/docs/api-reference/event_triggers.md +++ b/docs/api-reference/event_triggers.md @@ -440,3 +440,60 @@ def scroll_example(): width="100%", ) ``` + +## on_key_down + +The on_key_down event handler is called when the user presses a key while the element has focus. The handler receives the name of the key (e.g. `"Enter"`, `"Escape"`, `"a"`, `"ArrowUp"`) and a dictionary describing the active modifier keys (`alt_key`, `ctrl_key`, `meta_key`, `shift_key`). + +A handler may accept only the key name if it does not need the modifier info. + +```python demo exec +class KeyDownState(rx.State): + message: str = "" + + @rx.event + def handle_key_down(self, key: str): + if key == "Enter": + self.message = "You pressed Enter!" + else: + self.message = f"Last key pressed: {key}" + + +def key_down_example(): + return rx.vstack( + rx.text(KeyDownState.message), + rx.input( + placeholder="Focus me and press a key...", + on_key_down=KeyDownState.handle_key_down, + ), + ) +``` + +## on_key_up + +The on_key_up event handler is called when the user releases a key. It receives the same arguments as `on_key_down`. + +## Global Keyboard Events + +`on_key_down` on an element only fires while that element has focus. To listen for keyboard events anywhere on the page — for example, to implement global hotkeys — attach `on_key_down` to the `rx.window_event_listener` component, which listens on the browser window and renders no visible output. + +```python +class HotkeyState(rx.State): + last_key_pressed: str = "" + + @rx.event + def on_hotkey_press(self, key: str): + if key not in ("w", "a", "s", "d"): + return + self.last_key_pressed = key + + +def hotkey_example(): + return rx.vstack( + rx.text("Press w, a, s, or d anywhere on the page."), + rx.text(f"Last hotkey pressed: {HotkeyState.last_key_pressed}"), + rx.window_event_listener( + on_key_down=HotkeyState.on_hotkey_press, + ), + ) +``` diff --git a/docs/api-reference/special_events.md b/docs/api-reference/special_events.md index e6068dcf2b9..8227e5ff80a 100644 --- a/docs/api-reference/special_events.md +++ b/docs/api-reference/special_events.md @@ -29,6 +29,8 @@ 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 +To keep a container automatically scrolled to the bottom as new content is added — for example a chat or log view — use the [`rx.auto_scroll`](/docs/library/dynamic-rendering/auto-scroll/) component instead. + ## rx.redirect Redirect the user to a new path within the application. @@ -86,6 +88,29 @@ 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_clipboard` also accepts state Vars, so you can copy dynamic content, +such as the current value of an input field: + +```python demo exec +class CopyState(rx.State): + text: str = "" + + @rx.event + def set_text(self, value: str): + self.text = value + + +def copy_input_example(): + return rx.hstack( + rx.input( + placeholder="Type something to copy", + value=CopyState.text, + on_change=CopyState.set_text, + ), + rx.button("Copy", on_click=rx.set_clipboard(CopyState.text)), + ) +``` + ## rx.set_value Set the value of a specified reference element. @@ -130,3 +155,26 @@ rx.button( id="download button", ) ``` + +When the data to download is not already available at a known URL, return +`rx.download` from an event handler and pass the `data` directly: + +```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, + ) +``` diff --git a/docs/events/page_load_events.md b/docs/events/page_load_events.md index 7d6226b2f53..851e09ccce0 100644 --- a/docs/events/page_load_events.md +++ b/docs/events/page_load_events.md @@ -40,3 +40,42 @@ class State(rx.State): def index(): return rx.text("A Beautiful App") ``` + +## Handling Loading and Errors + +Avoid heavy synchronous work directly in an `on_load` handler — the page renders before the handler finishes, so a slow handler leaves the user staring at stale or empty data with no feedback. Instead: + +- Use an async handler for network or database calls so the event loop is not blocked. +- Set a loading flag and `yield` to send it to the frontend immediately, then render a spinner or placeholder with `rx.cond`. +- Wrap the work in `try`/`except` so a failure surfaces as an error message instead of a page that never finishes loading. + +```python +class DataState(rx.State): + data: dict = {} + loading: bool = False + error: str = "" + + @rx.event + async def load_initial_data(self): + self.loading = True + yield # Send the loading state to the frontend immediately. + try: + self.data = await fetch_data() + except Exception as e: + self.error = str(e) + finally: + self.loading = False + + +@rx.page(on_load=DataState.load_initial_data) +def index(): + return rx.cond( + DataState.loading, + rx.spinner(), + rx.cond( + DataState.error != "", + rx.text(f"Error: {DataState.error}"), + rx.text(f"Data loaded: {DataState.data}"), + ), + ) +``` diff --git a/docs/library/forms/text_area.md b/docs/library/forms/text_area.md index cc71e84a336..250a5439477 100644 --- a/docs/library/forms/text_area.md +++ b/docs/library/forms/text_area.md @@ -90,3 +90,51 @@ def feedback_form(): ), ) ``` + +## Submitting on Enter + +By default, pressing Enter in a text area inserts a new line. Set the +`enter_key_submit` prop to submit the enclosing form when Enter is pressed +instead. Shift+Enter still inserts a new line, so multi-line input remains +possible. + +```python demo exec +class EnterSubmitState(rx.State): + submitted_text: str = "" + + @rx.event + def handle_submit(self, form_data: dict): + self.submitted_text = form_data["message"] + + +def enter_key_submit_example(): + return rx.vstack( + rx.cond( + EnterSubmitState.submitted_text != "", + rx.text("Submitted: ", EnterSubmitState.submitted_text), + rx.text("Nothing submitted yet."), + ), + rx.form( + rx.text_area( + name="message", + placeholder="Type and press Enter to submit", + enter_key_submit=True, + ), + on_submit=EnterSubmitState.handle_submit, + ), + width="100%", + ) +``` + +`enter_key_submit` accepts a Var, so it can be toggled dynamically — for +example, disabling Enter-to-submit while a previous submission is still being +processed: `enter_key_submit=~State.is_loading`. + +```md alert warning +# `enter_key_submit` cannot be combined with `on_key_down`. + +The `enter_key_submit` prop is implemented with its own key down handler, so +passing both to the same component raises an error. If custom key handling is +needed, implement the Enter-to-submit behavior in your own `on_key_down` +handler instead. +``` diff --git a/docs/library/other/memo.md b/docs/library/other/memo.md index f56b2bbbe6e..6249f5bee27 100644 --- a/docs/library/other/memo.md +++ b/docs/library/other/memo.md @@ -68,6 +68,48 @@ def index(): ) ``` +## Using with `rx.foreach` + +To render a memoized component for each item of a list Var, wrap the call in a +lambda and pass the props by keyword. Also pass a `key` prop that uniquely +identifies each item — React uses it to track items across re-renders — but do +**not** declare `key` in the memo function's signature. It is forwarded +automatically and consumed by React. + +```python +from typing import TypedDict + + +class Task(TypedDict): + id: str + name: str + + +class TaskState(rx.State): + tasks: list[Task] = [ + {"id": "1", "name": "Write docs"}, + {"id": "2", "name": "Review PR"}, + ] + + +@rx.memo +def task_card(task: rx.Var[Task]) -> rx.Component: + return rx.card(rx.text(task["name"])) + + +def index(): + return rx.vstack( + rx.foreach( + TaskState.tasks, + lambda task: task_card(task=task, key=task["id"]), + ), + ) +``` + +Inside the memo body, `task` is a `Var`, not a plain dict: index into it with +`task["name"]` or use it in f-strings, but do not iterate over it or call +Python dict methods like `.keys()` — only Var operations are available. + ## Forwarding Props with `rx.RestProp` Use `rx.RestProp` to accept and forward arbitrary props (think `...rest` in JSX). Useful for thin wrappers that re-style a primitive without redeclaring every prop.