diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 018da581c..116a507a2 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -73,7 +73,7 @@ export const getToken = () => { * @param delta The delta to apply. */ export const applyDelta = (state, delta) => { - const new_state = {...state} + const new_state = { ...state } for (const substate in delta) { let s = new_state; const path = substate.split(".").slice(1); @@ -153,6 +153,15 @@ export const applyEvent = async (event, socket) => { navigator.clipboard.writeText(content); return false; } + if (event.name == "_download") { + const a = document.createElement('a'); + a.hidden = true; + a.href = event.payload.url; + a.download = event.payload.filename; + a.click(); + a.remove(); + return false; + } if (event.name == "_alert") { alert(event.payload.message); @@ -413,7 +422,7 @@ const applyClientStorageDelta = (client_storage, delta) => { for (const key in delta[substate]) { const state_key = `${substate}.${key}` if (client_storage.cookies && state_key in client_storage.cookies) { - const cookie_options = {...client_storage.cookies[state_key]} + const cookie_options = { ...client_storage.cookies[state_key] } const cookie_name = cookie_options.name || state_key delete cookie_options.name // name is not a valid cookie option cookies.set(cookie_name, delta[substate][key], cookie_options); @@ -445,18 +454,18 @@ export const useEventLoop = ( const router = useRouter() const [state, dispatch] = useReducer(applyDelta, initial_state) const [connectError, setConnectError] = useState(null) - + // Function to add new events to the event queue. const Event = (events, _e) => { - preventDefault(_e); - queueEvents(events, socket) + preventDefault(_e); + queueEvents(events, socket) } const sentHydrate = useRef(false); // Avoid double-hydrate due to React strict-mode // initial state hydrate useEffect(() => { if (router.isReady && !sentHydrate.current) { - Event(initial_events.map((e) => ({...e}))) + Event(initial_events.map((e) => ({ ...e }))) sentHydrate.current = true } }, [router.isReady]) diff --git a/reflex/__init__.py b/reflex/__init__.py index 53d0e0a38..15eabb0a7 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -23,6 +23,7 @@ from .event import EventChain as EventChain from .event import FileUpload as upload_files from .event import clear_local_storage as clear_local_storage from .event import console_log as console_log +from .event import download as download from .event import redirect as redirect from .event import remove_cookie as remove_cookie from .event import remove_local_storage as remove_local_storage diff --git a/reflex/event.py b/reflex/event.py index d098b0941..e7f3e547a 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -2,7 +2,7 @@ from __future__ import annotations import inspect -from typing import Any, Callable, Dict, List, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple from reflex import constants from reflex.base import Base @@ -330,6 +330,34 @@ def set_clipboard(content: str) -> EventSpec: ) +def download(url: str, filename: Optional[str] = None) -> EventSpec: + """Download the file at a given path. + + Args: + url : The URL to the file to download. + filename : The name that the file should be saved as after download. + + Raises: + ValueError: If the URL provided is invalid. + + Returns: + EventSpec: An event to download the associated file. + """ + if not url.startswith("/"): + raise ValueError("The URL argument should start with a /") + + # if filename is not provided, infer it from url + if filename is None: + filename = url.rpartition("/")[-1] + + return server_side( + "_download", + get_fn_signature(download), + url=url, + filename=filename, + ) + + def get_event(state, event): """Get the event from the given state.