diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcf3bef..ef6fefb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: - name: Check formatting run: cargo fmt --check - name: Clippy - run: cargo clippy --all-features --tests -- -D warnings + run: cargo clippy --features storage,docs,math --tests -- -D warnings - name: Run tests run: cargo test --all-features diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8eba55f..70a765e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,6 +23,6 @@ jobs: - name: Check formatting run: cargo fmt --check - name: Clippy - run: cargo clippy --all-features --tests -- -D warnings + run: cargo clippy --features storage,docs,math --tests -- -D warnings - name: Run tests run: cargo test --all-features diff --git a/.idea/leptos-use.iml b/.idea/leptos-use.iml index 9ce1da2..1384afa 100644 --- a/.idea/leptos-use.iml +++ b/.idea/leptos-use.iml @@ -43,6 +43,7 @@ + @@ -69,6 +70,7 @@ + diff --git a/Cargo.toml b/Cargo.toml index 47de1d8..f59bbf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } paste = "1" lazy_static = "1" +cfg-if = "1" [dependencies.web-sys] version = "0.3" @@ -66,6 +67,7 @@ features = [ docs = [] math = ["num"] storage = ["serde", "serde_json", "web-sys/StorageEvent"] +ssr = [] [package.metadata.docs.rs] all-features = true diff --git a/README.md b/README.md index b1fd6b9..fb3bcbc 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@

Crates.io + SSR Docs & Demos 41 Functions

diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 74c39d5..8900608 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -3,6 +3,7 @@ [Introduction](introduction.md) [Get Started](get_started.md) [Functions](functions.md) +[Server-Side Rendering](server_side_rendering.md) [Changelog](changelog.md) # @Storage diff --git a/docs/book/src/introduction.md b/docs/book/src/introduction.md index 5220748..14123ba 100644 --- a/docs/book/src/introduction.md +++ b/docs/book/src/introduction.md @@ -10,6 +10,7 @@

Crates.io + SSR Docs & Demos 41 Functions

diff --git a/docs/book/src/server_side_rendering.md b/docs/book/src/server_side_rendering.md new file mode 100644 index 0000000..ace770e --- /dev/null +++ b/docs/book/src/server_side_rendering.md @@ -0,0 +1,49 @@ +# Server-Side Rendering + +When using together with server-side rendering (SSR) you have to enable the feature `ssr` similar to +how you do it for `leptos`. + +In your Cargo.toml file add the following: + +```toml +... + +[features] +hydrate = [ + "leptos/hydrate", + ... +] +ssr = [ + ... + "leptos/ssr", + ... + "leptos-use/ssr" # add this +] + +... +``` + +Please see the `ssr` example in the examples folder for a simple working demonstration. + +Many functions work differently on the server and on the client. If that's the case you will +find information about these differences in their respective docs under the section "Server-Side Rendering". +If you don't find that section, it means that the function works exactly the same on both, the client +and the server. + +## Functions with Target Elements + +A lot of functions like `use_event_listener` and `use_element_size` are only useful when a target HTML/SVG element is +available. This is not the case on the server. You can simply wrap them in `create_effect` which will cause them to +be only called in the browser. + +```rust +create_effect( + cx, + move |_| { + // window() doesn't work on the server + use_event_listener(cx, window(), "resize", move |_| { + // ... + }) + }, +); +``` \ No newline at end of file diff --git a/examples/.cargo/config.toml b/examples/.cargo/config.toml index c841b61..c87f326 100644 --- a/examples/.cargo/config.toml +++ b/examples/.cargo/config.toml @@ -1,6 +1,2 @@ [build] rustflags = ["--cfg=web_sys_unstable_apis", "--cfg=has_std"] - -[unstable] -build-std = ["std", "panic_abort", "core", "alloc"] -build-std-features = ["panic_immediate_abort"] \ No newline at end of file diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 293966b..eabb6fd 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -38,6 +38,10 @@ members = [ "watch_throttled", ] +exclude = [ + "ssr" +] + [package] name = "leptos-use-examples" version = "0.3.3" diff --git a/examples/ssr/.gitignore b/examples/ssr/.gitignore new file mode 100644 index 0000000..a1e76e1 --- /dev/null +++ b/examples/ssr/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +pkg +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# node e2e test tools and outputs +node_modules/ +test-results/ +end2end/playwright-report/ +playwright/.cache/ diff --git a/examples/ssr/Cargo.toml b/examples/ssr/Cargo.toml new file mode 100644 index 0000000..1479522 --- /dev/null +++ b/examples/ssr/Cargo.toml @@ -0,0 +1,100 @@ +[package] +name = "start-axum" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +axum = { version = "0.6.4", optional = true } +console_error_panic_hook = "0.1" +console_log = "1" +cfg-if = "1" +leptos = { version = "0.4", features = ["nightly"] } +leptos_axum = { version = "0.4", optional = true } +leptos_meta = { version = "0.4", features = ["nightly"] } +leptos_router = { version = "0.4", features = ["nightly"] } +leptos-use = { path = "../..", features = ["storage"] } +log = "0.4" +simple_logger = "4" +tokio = { version = "1.25.0", optional = true } +tower = { version = "0.4.13", optional = true } +tower-http = { version = "0.4", features = ["fs"], optional = true } +wasm-bindgen = "=0.2.87" +thiserror = "1.0.38" +tracing = { version = "0.1.37", optional = true } +http = "0.2.8" + +[features] +hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] +ssr = [ + "dep:axum", + "dep:tokio", + "dep:tower", + "dep:tower-http", + "dep:leptos_axum", + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", + "dep:tracing", + "leptos-use/ssr" +] + +[package.metadata.cargo-all-features] +denylist = ["axum", "tokio", "tower", "tower-http", "leptos_axum"] +skip_feature_sets = [["ssr", "hydrate"]] + +[package.metadata.leptos] +# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name +output-name = "start-axum" + +# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. +site-root = "target/site" + +# The site-root relative folder where all compiled output (JS, WASM and CSS) is written +# Defaults to pkg +site-pkg-dir = "pkg" + +# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to //app.css +style-file = "style/main.scss" +# Assets source dir. All files found here will be copied and synchronized to site-root. +# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir. +# +# Optional. Env: LEPTOS_ASSETS_DIR. +assets-dir = "public" + +# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. +site-addr = "127.0.0.1:3000" + +# The port to use for automatic reload monitoring +reload-port = 3001 + +# The browserlist query used for optimizing the CSS. +browserquery = "defaults" + +# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head +watch = false + +# The environment Leptos will run in, usually either "DEV" or "PROD" +env = "DEV" + +# The features to use when compiling the bin target +# +# Optional. Can be over-ridden with the command line parameter --bin-features +bin-features = ["ssr"] + +# If the --no-default-features flag should be used when compiling the bin target +# +# Optional. Defaults to false. +bin-default-features = false + +# The features to use when compiling the lib target +# +# Optional. Can be over-ridden with the command line parameter --lib-features +lib-features = ["hydrate"] + +# If the --no-default-features flag should be used when compiling the lib target +# +# Optional. Defaults to false. +lib-default-features = false diff --git a/examples/ssr/README.md b/examples/ssr/README.md new file mode 100644 index 0000000..cab9181 --- /dev/null +++ b/examples/ssr/README.md @@ -0,0 +1,56 @@ +# Leptos-Use SSR Example + +## Running the example + +```bash +cargo leptos watch +``` + +## Installing Additional Tools + +By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools. + +1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly +2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly +3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future) +4. `npm install -g sass` - install `dart-sass` (should be optional in future + +## Compiling for Release +```bash +cargo leptos build --release +``` + +Will generate your server binary in target/server/release and your site package in target/site + +## Testing Your Project +```bash +cargo leptos end-to-end +``` + +```bash +cargo leptos end-to-end --release +``` + +Cargo-leptos uses Playwright as the end-to-end test tool. +Tests are located in end2end/tests directory. + +## Executing a Server on a Remote Machine Without the Toolchain +After running a `cargo leptos build --release` the minimum files needed are: + +1. The server binary located in `target/server/release` +2. The `site` directory and all files within located in `target/site` + +Copy these files to your remote server. The directory structure should be: +```text +start-axum +site/ +``` +Set the following environment variables (updating for your project as needed): +```text +LEPTOS_OUTPUT_NAME="start-axum" +LEPTOS_SITE_ROOT="site" +LEPTOS_SITE_PKG_DIR="pkg" +LEPTOS_SITE_ADDR="127.0.0.1:3000" +LEPTOS_RELOAD_PORT="3001" +``` +Finally, run the server binary. diff --git a/examples/ssr/public/favicon.ico b/examples/ssr/public/favicon.ico new file mode 100644 index 0000000..2ba8527 Binary files /dev/null and b/examples/ssr/public/favicon.ico differ diff --git a/examples/ssr/rust-toolchain.toml b/examples/ssr/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/examples/ssr/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/examples/ssr/src/app.rs b/examples/ssr/src/app.rs new file mode 100644 index 0000000..99647ed --- /dev/null +++ b/examples/ssr/src/app.rs @@ -0,0 +1,79 @@ +use crate::error_template::{AppError, ErrorTemplate}; +use leptos::ev::{keypress, KeyboardEvent}; +use leptos::*; +use leptos_meta::*; +use leptos_router::*; +use leptos_use::storage::use_local_storage; +use leptos_use::{ + use_debounce_fn, use_event_listener, use_intl_number_format, UseIntlNumberFormatOptions, +}; + +#[component] +pub fn App(cx: Scope) -> impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(cx); + + view! { + cx, + + + + + + <Router fallback=|cx| { + let mut outside_errors = Errors::default(); + outside_errors.insert_with_default_key(AppError::NotFound); + view! { cx, + <ErrorTemplate outside_errors/> + } + .into_view(cx) + }> + <main> + <Routes> + <Route path="" view=|cx| view! { cx, <HomePage/> }/> + </Routes> + </main> + </Router> + } +} + +/// Renders the home page of your application. +#[component] +fn HomePage(cx: Scope) -> impl IntoView { + // Creates a reactive value to update the button + let (count, set_count, _) = use_local_storage(cx, "count-state", 0); + let on_click = move |_| set_count.update(|count| *count += 1); + + let nf = use_intl_number_format( + UseIntlNumberFormatOptions::default().locale("zh-Hans-CN-u-nu-hanidec"), + ); + + let zh_count = nf.format::<i32>(cx, count); + + let (key, set_key) = create_signal(cx, "".to_string()); + + create_effect(cx, move |_| { + // window() doesn't work on the server + let _ = use_event_listener(cx, window(), keypress, move |evt: KeyboardEvent| { + set_key(evt.key()) + }); + }); + + let (debounce_value, set_debounce_value) = create_signal(cx, "not called"); + + let debounced_fn = use_debounce_fn( + move || { + set_debounce_value("called"); + }, + 2000.0, + ); + debounced_fn(); + + view! { cx, + <h1>"Leptos-Use SSR Example"</h1> + <button on:click=on_click>"Click Me: " { count }</button> + <p>"Locale \"zh-Hans-CN-u-nu-hanidec\": " { zh_count }</p> + <p>"Press any key: " { key }</p> + <p>"Debounced called: " { debounce_value }</p> + } +} diff --git a/examples/ssr/src/error_template.rs b/examples/ssr/src/error_template.rs new file mode 100644 index 0000000..3a947fa --- /dev/null +++ b/examples/ssr/src/error_template.rs @@ -0,0 +1,76 @@ +use cfg_if::cfg_if; +use http::status::StatusCode; +use leptos::*; +use thiserror::Error; + +#[cfg(feature = "ssr")] +use leptos_axum::ResponseOptions; + +#[derive(Clone, Debug, Error)] +pub enum AppError { + #[error("Not Found")] + NotFound, +} + +impl AppError { + pub fn status_code(&self) -> StatusCode { + match self { + AppError::NotFound => StatusCode::NOT_FOUND, + } + } +} + +// A basic function to display errors served by the error boundaries. +// Feel free to do more complicated things here than just displaying the error. +#[component] +pub fn ErrorTemplate( + cx: Scope, + #[prop(optional)] outside_errors: Option<Errors>, + #[prop(optional)] errors: Option<RwSignal<Errors>>, +) -> impl IntoView { + let errors = match outside_errors { + Some(e) => create_rw_signal(cx, e), + None => match errors { + Some(e) => e, + None => panic!("No Errors found and we expected errors!"), + }, + }; + // Get Errors from Signal + let errors = errors.get(); + + // Downcast lets us take a type that implements `std::error::Error` + let errors: Vec<AppError> = errors + .into_iter() + .filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned()) + .collect(); + println!("Errors: {errors:#?}"); + + // Only the response code for the first error is actually sent from the server + // this may be customized by the specific application + cfg_if! { if #[cfg(feature="ssr")] { + let response = use_context::<ResponseOptions>(cx); + if let Some(response) = response { + response.set_status(errors[0].status_code()); + } + }} + + view! {cx, + <h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1> + <For + // a function that returns the items we're iterating over; a signal is fine + each= move || {errors.clone().into_iter().enumerate()} + // a unique key for each item as a reference + key=|(index, _error)| *index + // renders each item to a view + view= move |cx, error| { + let error_string = error.1.to_string(); + let error_code= error.1.status_code(); + view! { + cx, + <h2>{error_code.to_string()}</h2> + <p>"Error: " {error_string}</p> + } + } + /> + } +} diff --git a/examples/ssr/src/fileserv.rs b/examples/ssr/src/fileserv.rs new file mode 100644 index 0000000..07fade7 --- /dev/null +++ b/examples/ssr/src/fileserv.rs @@ -0,0 +1,40 @@ +use cfg_if::cfg_if; + +cfg_if! { if #[cfg(feature = "ssr")] { + use axum::{ + body::{boxed, Body, BoxBody}, + extract::State, + response::IntoResponse, + http::{Request, Response, StatusCode, Uri}, + }; + use axum::response::Response as AxumResponse; + use tower::ServiceExt; + use tower_http::services::ServeDir; + use leptos::*; + use crate::app::App; + + pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse { + let root = options.site_root.clone(); + let res = get_static_file(uri.clone(), &root).await.unwrap(); + + if res.status() == StatusCode::OK { + res.into_response() + } else { + let handler = leptos_axum::render_app_to_stream(options.to_owned(), move |cx| view!{cx, <App/>}); + handler(req).await.into_response() + } + } + + async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> { + let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` + // This path is relative to the cargo root + match ServeDir::new(root).oneshot(req).await { + Ok(res) => Ok(res.map(boxed)), + Err(err) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {err}"), + )), + } + } +}} diff --git a/examples/ssr/src/lib.rs b/examples/ssr/src/lib.rs new file mode 100644 index 0000000..7190a49 --- /dev/null +++ b/examples/ssr/src/lib.rs @@ -0,0 +1,21 @@ +use cfg_if::cfg_if; +pub mod app; +pub mod error_template; +pub mod fileserv; + +cfg_if! { if #[cfg(feature = "hydrate")] { + use leptos::*; + use wasm_bindgen::prelude::wasm_bindgen; + use crate::app::*; + + #[wasm_bindgen] + pub fn hydrate() { + // initializes logging using the `log` crate + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + leptos::mount_to_body(move |cx| { + view! { cx, <App/> } + }); + } +}} diff --git a/examples/ssr/src/main.rs b/examples/ssr/src/main.rs new file mode 100644 index 0000000..1930efc --- /dev/null +++ b/examples/ssr/src/main.rs @@ -0,0 +1,43 @@ +#[cfg(feature = "ssr")] +#[tokio::main] +async fn main() { + use axum::{routing::post, Router}; + use leptos::*; + use leptos_axum::{generate_route_list, LeptosRoutes}; + use start_axum::app::*; + use start_axum::fileserv::file_and_error_handler; + + simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging"); + + // Setting get_configuration(None) means we'll be using cargo-leptos's env values + // For deployment these variables are: + // <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain> + // Alternately a file can be specified such as Some("Cargo.toml") + // The file would need to be included with the executable when moved to deployment + let conf = get_configuration(None).await.unwrap(); + let leptos_options = conf.leptos_options; + let addr = leptos_options.site_addr; + let routes = generate_route_list(|cx| view! { cx, <App/> }).await; + + // build our application with a route + let app = Router::new() + .route("/api/*fn_name", post(leptos_axum::handle_server_fns)) + .leptos_routes(&leptos_options, routes, |cx| view! { cx, <App/> }) + .fallback(file_and_error_handler) + .with_state(leptos_options); + + // run our app with hyper + // `axum::Server` is a re-export of `hyper::Server` + log!("listening on http://{}", &addr); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +#[cfg(not(feature = "ssr"))] +pub fn main() { + // no client-side main function + // unless we want this to work with e.g., Trunk for a purely client-side app + // see lib.rs for hydration function instead +} diff --git a/examples/ssr/style/main.scss b/examples/ssr/style/main.scss new file mode 100644 index 0000000..e4538e1 --- /dev/null +++ b/examples/ssr/style/main.scss @@ -0,0 +1,4 @@ +body { + font-family: sans-serif; + text-align: center; +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index b0a89db..6008a83 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ // #![feature(doc_cfg)] //! Collection of essential Leptos utilities inspired by SolidJS USE / VueUse +use cfg_if::cfg_if; + pub mod core; #[cfg(feature = "docs")] pub mod docs; @@ -10,15 +12,13 @@ pub mod math; pub mod storage; pub mod utils; -#[cfg(web_sys_unstable_apis)] -mod use_element_size; -#[cfg(web_sys_unstable_apis)] -mod use_resize_observer; +cfg_if! { if #[cfg(web_sys_unstable_apis)] { + mod use_element_size; + mod use_resize_observer; -#[cfg(web_sys_unstable_apis)] -pub use use_element_size::*; -#[cfg(web_sys_unstable_apis)] -pub use use_resize_observer::*; + pub use use_element_size::*; + pub use use_resize_observer::*; +}} mod on_click_outside; mod use_active_element; diff --git a/src/on_click_outside.rs b/src/on_click_outside.rs index 5167099..6f4dd01 100644 --- a/src/on_click_outside.rs +++ b/src/on_click_outside.rs @@ -45,6 +45,10 @@ static IOS_WORKAROUND: RwLock<bool> = RwLock::new(false); /// which is **not** supported by IE 11, Edge 18 and below. /// If you are targeting these browsers, we recommend you to include /// [this code snippet](https://gist.github.com/sibbng/13e83b1dd1b733317ce0130ef07d4efd) on your project. +/// +/// ## Server-Side Rendering +/// +/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) pub fn on_click_outside<El, T, F>(cx: Scope, target: El, handler: F) -> impl FnOnce() + Clone where El: Clone, diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 511275d..d517ccf 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -1,8 +1,11 @@ +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports, dead_code))] + use crate::utils::{CloneableFn, CloneableFnWithArg, FilterOptions}; use crate::{ filter_builder_methods, use_event_listener, watch_pausable_with_options, DebounceOptions, ThrottleOptions, WatchOptions, WatchPausableReturn, }; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use js_sys::Reflect; use leptos::*; @@ -144,6 +147,10 @@ const CUSTOM_STORAGE_EVENT_NAME: &str = "leptos-use-storage"; /// /// You can specify `debounce` or `throttle` options for limiting writes to storage. /// +/// ## Server-Side Rendering +/// +/// On the server this falls back to a `create_signal(cx, default)` and an empty remove function. +/// /// ## See also /// /// * [`use_local_storage`] @@ -188,192 +195,196 @@ where let (data, set_data) = create_signal(cx, defaults.get_untracked()); - let storage = storage_type.into_storage(); + cfg_if! { if #[cfg(feature = "ssr")] { + let remove: Box<dyn CloneableFn> = Box::new(|| {}); + } else { + let storage = storage_type.into_storage(); - let remove: Box<dyn CloneableFn> = match storage { - Ok(Some(storage)) => { - let on_err = on_error.clone(); + let remove: Box<dyn CloneableFn> = match storage { + Ok(Some(storage)) => { + let on_err = on_error.clone(); - let store = storage.clone(); - let k = key.to_string(); + let store = storage.clone(); + let k = key.to_string(); - let write = move |v: &T| { - match serde_json::to_string(&v) { - Ok(ref serialized) => match store.get_item(&k) { - Ok(old_value) => { - if old_value.as_ref() != Some(serialized) { - if let Err(e) = store.set_item(&k, serialized) { - on_err(UseStorageError::StorageAccessError(e)); - } else { - let mut event_init = web_sys::CustomEventInit::new(); - event_init.detail( - &StorageEventDetail { - key: Some(k.clone()), - old_value, - new_value: Some(serialized.clone()), - storage_area: Some(store.clone()), - } - .into(), - ); + let write = move |v: &T| { + match serde_json::to_string(&v) { + Ok(ref serialized) => match store.get_item(&k) { + Ok(old_value) => { + if old_value.as_ref() != Some(serialized) { + if let Err(e) = store.set_item(&k, serialized) { + on_err(UseStorageError::StorageAccessError(e)); + } else { + let mut event_init = web_sys::CustomEventInit::new(); + event_init.detail( + &StorageEventDetail { + key: Some(k.clone()), + old_value, + new_value: Some(serialized.clone()), + storage_area: Some(store.clone()), + } + .into(), + ); - // importantly this should _not_ be a StorageEvent since those cannot - // be constructed with a non-built-in storage area - let _ = window().dispatch_event( - &web_sys::CustomEvent::new_with_event_init_dict( - CUSTOM_STORAGE_EVENT_NAME, - &event_init, - ) - .expect("Failed to create CustomEvent"), - ); + // importantly this should _not_ be a StorageEvent since those cannot + // be constructed with a non-built-in storage area + let _ = window().dispatch_event( + &web_sys::CustomEvent::new_with_event_init_dict( + CUSTOM_STORAGE_EVENT_NAME, + &event_init, + ) + .expect("Failed to create CustomEvent"), + ); + } } } - } - Err(e) => { - on_err.clone()(UseStorageError::StorageAccessError(e)); - } - }, - Err(e) => { - on_err.clone()(UseStorageError::SerializationError(e)); - } - } - }; - - let store = storage.clone(); - let on_err = on_error.clone(); - let k = key.to_string(); - let def = defaults.clone(); - - let read = move |event_detail: Option<StorageEventDetail>| -> Option<T> { - let raw_init = match serde_json::to_string(&def.get_untracked()) { - Ok(serialized) => Some(serialized), - Err(e) => { - on_err.clone()(UseStorageError::DefaultSerializationError(e)); - None - } - }; - - let raw_value = if let Some(event_detail) = event_detail { - event_detail.new_value - } else { - match store.get_item(&k) { - Ok(raw_value) => match raw_value { - Some(raw_value) => { - Some(merge_defaults(&raw_value, &def.get_untracked())) + Err(e) => { + on_err.clone()(UseStorageError::StorageAccessError(e)); } - None => raw_init.clone(), }, Err(e) => { - on_err.clone()(UseStorageError::StorageAccessError(e)); - None + on_err.clone()(UseStorageError::SerializationError(e)); } } }; - match raw_value { - Some(raw_value) => match serde_json::from_str(&raw_value) { - Ok(v) => Some(v), + let store = storage.clone(); + let on_err = on_error.clone(); + let k = key.to_string(); + let def = defaults.clone(); + + let read = move |event_detail: Option<StorageEventDetail>| -> Option<T> { + let raw_init = match serde_json::to_string(&def.get_untracked()) { + Ok(serialized) => Some(serialized), Err(e) => { - on_err.clone()(UseStorageError::SerializationError(e)); + on_err.clone()(UseStorageError::DefaultSerializationError(e)); None } - }, - None => { - if let Some(raw_init) = &raw_init { - if write_defaults { - if let Err(e) = store.set_item(&k, raw_init) { - on_err(UseStorageError::StorageAccessError(e)); + }; + + let raw_value = if let Some(event_detail) = event_detail { + event_detail.new_value + } else { + match store.get_item(&k) { + Ok(raw_value) => match raw_value { + Some(raw_value) => { + Some(merge_defaults(&raw_value, &def.get_untracked())) } - } - } - - Some(def.get_untracked()) - } - } - }; - - let WatchPausableReturn { - pause: pause_watch, - resume: resume_watch, - .. - } = watch_pausable_with_options( - cx, - move || data.get(), - move |data, _, _| write.clone()(data), - WatchOptions::default().filter(filter), - ); - - let k = key.to_string(); - let store = storage.clone(); - - let update = move |event_detail: Option<StorageEventDetail>| { - if let Some(event_detail) = &event_detail { - if event_detail.storage_area != Some(store) { - return; - } - - match &event_detail.key { - None => { - set_data.set(defaults.get_untracked()); - return; - } - Some(event_key) => { - if event_key != &k { - return; + None => raw_init.clone(), + }, + Err(e) => { + on_err.clone()(UseStorageError::StorageAccessError(e)); + None } } }; - } - pause_watch(); + match raw_value { + Some(raw_value) => match serde_json::from_str(&raw_value) { + Ok(v) => Some(v), + Err(e) => { + on_err.clone()(UseStorageError::SerializationError(e)); + None + } + }, + None => { + if let Some(raw_init) = &raw_init { + if write_defaults { + if let Err(e) = store.set_item(&k, raw_init) { + on_err(UseStorageError::StorageAccessError(e)); + } + } + } - if let Some(value) = read(event_detail.clone()) { - set_data.set(value); - } + Some(def.get_untracked()) + } + } + }; - if event_detail.is_some() { - // use timeout to avoid inifinite loop - let resume = resume_watch.clone(); - let _ = set_timeout_with_handle(resume, Duration::ZERO); - } else { - resume_watch(); - } - }; - - let upd = update.clone(); - let update_from_custom_event = - move |event: web_sys::CustomEvent| upd.clone()(Some(event.into())); - - let upd = update.clone(); - let update_from_storage_event = - move |event: web_sys::StorageEvent| upd.clone()(Some(event.into())); - - if listen_to_storage_changes { - let _ = use_event_listener(cx, window(), ev::storage, update_from_storage_event); - let _ = use_event_listener( + let WatchPausableReturn { + pause: pause_watch, + resume: resume_watch, + .. + } = watch_pausable_with_options( cx, - window(), - ev::Custom::new(CUSTOM_STORAGE_EVENT_NAME), - update_from_custom_event, + move || data.get(), + move |data, _, _| write.clone()(data), + WatchOptions::default().filter(filter), ); + + let k = key.to_string(); + let store = storage.clone(); + + let update = move |event_detail: Option<StorageEventDetail>| { + if let Some(event_detail) = &event_detail { + if event_detail.storage_area != Some(store) { + return; + } + + match &event_detail.key { + None => { + set_data.set(defaults.get_untracked()); + return; + } + Some(event_key) => { + if event_key != &k { + return; + } + } + }; + } + + pause_watch(); + + if let Some(value) = read(event_detail.clone()) { + set_data.set(value); + } + + if event_detail.is_some() { + // use timeout to avoid inifinite loop + let resume = resume_watch.clone(); + let _ = set_timeout_with_handle(resume, Duration::ZERO); + } else { + resume_watch(); + } + }; + + let upd = update.clone(); + let update_from_custom_event = + move |event: web_sys::CustomEvent| upd.clone()(Some(event.into())); + + let upd = update.clone(); + let update_from_storage_event = + move |event: web_sys::StorageEvent| upd.clone()(Some(event.into())); + + if listen_to_storage_changes { + let _ = use_event_listener(cx, window(), ev::storage, update_from_storage_event); + let _ = use_event_listener( + cx, + window(), + ev::Custom::new(CUSTOM_STORAGE_EVENT_NAME), + update_from_custom_event, + ); + } + + update(None); + + let k = key.to_string(); + + Box::new(move || { + let _ = storage.remove_item(&k); + }) } - - update(None); - - let k = key.to_string(); - - Box::new(move || { - let _ = storage.remove_item(&k); - }) - } - Err(e) => { - on_error(UseStorageError::NoStorage(e)); - Box::new(move || {}) - } - _ => { - // do nothing - Box::new(move || {}) - } - }; + Err(e) => { + on_error(UseStorageError::NoStorage(e)); + Box::new(move || {}) + } + _ => { + // do nothing + Box::new(move || {}) + } + }; + }} (data, set_data, move || remove.clone()()) } diff --git a/src/use_active_element.rs b/src/use_active_element.rs index d66d498..c60aabd 100644 --- a/src/use_active_element.rs +++ b/src/use_active_element.rs @@ -1,4 +1,7 @@ +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] + use crate::use_event_listener_with_options; +use cfg_if::cfg_if; use leptos::ev::{blur, focus}; use leptos::html::{AnyElement, ToHtmlElement}; use leptos::*; @@ -27,42 +30,51 @@ use web_sys::AddEventListenerOptions; /// # view! { cx, } /// # } /// ``` - +/// +/// ## Server-Side Rendering +/// +/// On the server this returns a `Signal` that always contains the value `None`. pub fn use_active_element(cx: Scope) -> Signal<Option<HtmlElement<AnyElement>>> { - let get_active_element = move || { - document() - .active_element() - .map(|el| el.to_leptos_element(cx)) - }; + cfg_if! { if #[cfg(feature = "ssr")] { + let get_active_element = || { None }; + } else { + let get_active_element = move || { + document() + .active_element() + .map(|el| el.to_leptos_element(cx)) + }; + }} let (active_element, set_active_element) = create_signal(cx, get_active_element()); - let mut listener_options = AddEventListenerOptions::new(); - listener_options.capture(true); + cfg_if! { if #[cfg(not(feature = "ssr"))] { + let mut listener_options = AddEventListenerOptions::new(); + listener_options.capture(true); - let _ = use_event_listener_with_options( - cx, - window(), - blur, - move |event| { - if event.related_target().is_some() { - return; - } + let _ = use_event_listener_with_options( + cx, + window(), + blur, + move |event| { + if event.related_target().is_some() { + return; + } - set_active_element.update(|el| *el = get_active_element()); - }, - listener_options.clone(), - ); + set_active_element.update(|el| *el = get_active_element()); + }, + listener_options.clone(), + ); - let _ = use_event_listener_with_options( - cx, - window(), - focus, - move |_| { - set_active_element.update(|el| *el = get_active_element()); - }, - listener_options, - ); + let _ = use_event_listener_with_options( + cx, + window(), + focus, + move |_| { + set_active_element.update(|el| *el = get_active_element()); + }, + listener_options, + ); + }} active_element.into() } diff --git a/src/use_breakpoints.rs b/src/use_breakpoints.rs index b8c88e7..ebc7aee 100644 --- a/src/use_breakpoints.rs +++ b/src/use_breakpoints.rs @@ -103,6 +103,11 @@ use std::hash::Hash; /// # view! { cx, } /// # } /// ``` +/// +/// ## Server-Side Rendering +/// +/// Since internally this uses [`use_media_query`], which returns always `false` on the server, +/// the returned methods also will return `false`. pub fn use_breakpoints<K: Eq + Hash + Debug + Clone>( cx: Scope, breakpoints: HashMap<K, u32>, diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index bc595a0..8259b78 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -8,6 +8,7 @@ use std::fmt::{Display, Formatter}; use crate::core::StorageType; use crate::use_preferred_dark; use crate::utils::CloneableFnWithArg; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::*; use std::marker::PhantomData; @@ -88,6 +89,10 @@ use wasm_bindgen::JsCast; /// # } /// ``` /// +/// ## Server-Side Rendering +/// +/// On the server this will by default return `ColorMode::Light`. Persistence is disabled, of course. +/// /// ## See also /// /// * [`use_dark`] @@ -247,9 +252,9 @@ where } } -#[cfg(not(feature = "storage"))] /// Color modes -#[derive(Clone, Default, PartialEq, Eq, Hash)] +#[derive(Clone, Default, PartialEq, Eq, Hash, Debug)] +#[cfg_attr(feature = "storage", derive(Serialize, Deserialize))] pub enum ColorMode { #[default] Auto, @@ -258,61 +263,50 @@ pub enum ColorMode { Custom(String), } -#[cfg(not(feature = "storage"))] -fn get_store_signal( - cx: Scope, - initial_value: MaybeSignal<ColorMode>, - storage_signal: Option<RwSignal<ColorMode>>, - _storage_key: &String, - _storage_enabled: bool, - _storage: StorageType, - _listen_to_storage_changes: bool, -) -> (ReadSignal<ColorMode>, WriteSignal<ColorMode>) { - if let Some(storage_signal) = storage_signal { - storage_signal.split() - } else { - create_signal(cx, initial_value.get_untracked()) +cfg_if! { if #[cfg(feature = "storage")] { + fn get_store_signal( + cx: Scope, + initial_value: MaybeSignal<ColorMode>, + storage_signal: Option<RwSignal<ColorMode>>, + storage_key: &str, + storage_enabled: bool, + storage: StorageType, + listen_to_storage_changes: bool, + ) -> (ReadSignal<ColorMode>, WriteSignal<ColorMode>) { + if let Some(storage_signal) = storage_signal { + storage_signal.split() + } else if storage_enabled { + let (store, set_store, _) = use_storage_with_options( + cx, + storage_key, + initial_value.get_untracked(), + UseStorageOptions::default() + .listen_to_storage_changes(listen_to_storage_changes) + .storage_type(storage), + ); + + (store, set_store) + } else { + create_signal(cx, initial_value.get_untracked()) + } } -} - -#[cfg(feature = "storage")] -/// Color modes -#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash, Debug)] -pub enum ColorMode { - #[default] - Auto, - Light, - Dark, - Custom(String), -} - -#[cfg(feature = "storage")] -fn get_store_signal( - cx: Scope, - initial_value: MaybeSignal<ColorMode>, - storage_signal: Option<RwSignal<ColorMode>>, - storage_key: &str, - storage_enabled: bool, - storage: StorageType, - listen_to_storage_changes: bool, -) -> (ReadSignal<ColorMode>, WriteSignal<ColorMode>) { - if let Some(storage_signal) = storage_signal { - storage_signal.split() - } else if storage_enabled { - let (store, set_store, _) = use_storage_with_options( - cx, - storage_key, - initial_value.get_untracked(), - UseStorageOptions::default() - .listen_to_storage_changes(listen_to_storage_changes) - .storage_type(storage), - ); - - (store, set_store) - } else { - create_signal(cx, initial_value.get_untracked()) +} else { + fn get_store_signal( + cx: Scope, + initial_value: MaybeSignal<ColorMode>, + storage_signal: Option<RwSignal<ColorMode>>, + _storage_key: &String, + _storage_enabled: bool, + _storage: StorageType, + _listen_to_storage_changes: bool, + ) -> (ReadSignal<ColorMode>, WriteSignal<ColorMode>) { + if let Some(storage_signal) = storage_signal { + storage_signal.split() + } else { + create_signal(cx, initial_value.get_untracked()) + } } -} +}} impl Display for ColorMode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { diff --git a/src/use_css_var.rs b/src/use_css_var.rs index 88ddccd..e1dd70f 100644 --- a/src/use_css_var.rs +++ b/src/use_css_var.rs @@ -1,5 +1,8 @@ +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] + use crate::core::ElementMaybeSignal; use crate::{use_mutation_observer_with_options, watch, watch_with_options, WatchOptions}; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::*; use std::marker::PhantomData; @@ -69,7 +72,10 @@ use wasm_bindgen::{JsCast, JsValue}; /// } /// # } /// ``` - +/// +/// ## Server-Side Rendering +/// +/// On the server this simply returns `create_signal(cx, options.initial_value)`. pub fn use_css_var( cx: Scope, prop: impl Into<MaybeSignal<String>>, @@ -98,72 +104,74 @@ where let (variable, set_variable) = create_signal(cx, initial_value.clone()); - let el_signal = (cx, target).into(); - let prop = prop.into(); + cfg_if! { if #[cfg(not(feature = "ssr"))] { + let el_signal = (cx, target).into(); + let prop = prop.into(); - let update_css_var = { - let prop = prop.clone(); - let el_signal = el_signal.clone(); + let update_css_var = { + let prop = prop.clone(); + let el_signal = el_signal.clone(); - move || { - let key = prop.get_untracked(); + move || { + let key = prop.get_untracked(); - if let Some(el) = el_signal.get_untracked() { - if let Ok(Some(style)) = window().get_computed_style(&el.into()) { - if let Ok(value) = style.get_property_value(&key) { - set_variable.update(|var| *var = value.trim().to_string()); - return; + if let Some(el) = el_signal.get_untracked() { + if let Ok(Some(style)) = window().get_computed_style(&el.into()) { + if let Ok(value) = style.get_property_value(&key) { + set_variable.update(|var| *var = value.trim().to_string()); + return; + } } + + let initial_value = initial_value.clone(); + set_variable.update(|var| *var = initial_value); } - - let initial_value = initial_value.clone(); - set_variable.update(|var| *var = initial_value); } + }; + + if observe { + let mut init = web_sys::MutationObserverInit::new(); + let update_css_var = update_css_var.clone(); + let el_signal = el_signal.clone(); + + init.attribute_filter(&js_sys::Array::from_iter( + vec![JsValue::from_str("style")].into_iter(), + )); + use_mutation_observer_with_options::<ElementMaybeSignal<T, web_sys::Element>, T, _>( + cx, + el_signal, + move |_, _| update_css_var(), + init, + ); } - }; - if observe { - let mut init = web_sys::MutationObserverInit::new(); - let update_css_var = update_css_var.clone(); - let el_signal = el_signal.clone(); + // To get around style attributes on node_refs that are not applied after the first render + set_timeout(update_css_var.clone(), Duration::ZERO); - init.attribute_filter(&js_sys::Array::from_iter( - vec![JsValue::from_str("style")].into_iter(), - )); - use_mutation_observer_with_options::<ElementMaybeSignal<T, web_sys::Element>, T, _>( + { + let el_signal = el_signal.clone(); + let prop = prop.clone(); + + let _ = watch_with_options( + cx, + move || (el_signal.get(), prop.get()), + move |_, _, _| update_css_var(), + WatchOptions::default().immediate(true), + ); + } + + let _ = watch( cx, - el_signal, - move |_, _| update_css_var(), - init, + move || variable.get(), + move |val, _, _| { + if let Some(el) = el_signal.get() { + let el = el.into().unchecked_into::<web_sys::HtmlElement>(); + let style = el.style(); + let _ = style.set_property(&prop.get_untracked(), val); + } + }, ); - } - - // To get around style attributes on node_refs that are not applied after the first render - set_timeout(update_css_var.clone(), Duration::ZERO); - - { - let el_signal = el_signal.clone(); - let prop = prop.clone(); - - let _ = watch_with_options( - cx, - move || (el_signal.get(), prop.get()), - move |_, _, _| update_css_var(), - WatchOptions::default().immediate(true), - ); - } - - let _ = watch( - cx, - move || variable.get(), - move |val, _, _| { - if let Some(el) = el_signal.get() { - let el = el.into().unchecked_into::<web_sys::HtmlElement>(); - let style = el.style(); - let _ = style.set_property(&prop.get_untracked(), val); - } - }, - ); + }} (variable, set_variable) } @@ -192,13 +200,26 @@ where _marker: PhantomData<T>, } -impl Default for UseCssVarOptions<web_sys::Element, web_sys::Element> { - fn default() -> Self { - Self { - target: document().document_element().expect("No document element"), - initial_value: "".into(), - observe: false, - _marker: PhantomData, +cfg_if! { if #[cfg(feature = "ssr")] { + impl Default for UseCssVarOptions<Option<web_sys::Element>, web_sys::Element> { + fn default() -> Self { + Self { + target: None, + initial_value: "".into(), + observe: false, + _marker: PhantomData, + } } } -} +} else { + impl Default for UseCssVarOptions<web_sys::Element, web_sys::Element> { + fn default() -> Self { + Self { + target: document().document_element().expect("No document element"), + initial_value: "".into(), + observe: false, + _marker: PhantomData, + } + } + } +}} diff --git a/src/use_cycle_list.rs b/src/use_cycle_list.rs index a248b0b..a4d16c9 100644 --- a/src/use_cycle_list.rs +++ b/src/use_cycle_list.rs @@ -31,7 +31,6 @@ use leptos::*; /// # view! { cx, } /// # } /// ``` - pub fn use_cycle_list<T, L>( cx: Scope, list: L, diff --git a/src/use_debounce_fn.rs b/src/use_debounce_fn.rs index b073fa1..44c938a 100644 --- a/src/use_debounce_fn.rs +++ b/src/use_debounce_fn.rs @@ -67,6 +67,11 @@ use std::rc::Rc; /// /// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle) /// - [Debouncing and Throttling Explained Through Examples](https://css-tricks.com/debouncing-throttling-explained-examples/) +/// +/// ## Server-Side Rendering +/// +/// Internally this uses `setTimeout` which is not supported on the server. So usually calling +/// a debounced function on the server will simply be ignored. pub fn use_debounce_fn<F, R>( func: F, ms: impl Into<MaybeSignal<f64>> + 'static, diff --git a/src/use_document_visibility.rs b/src/use_document_visibility.rs index 18a6895..4b0185b 100644 --- a/src/use_document_visibility.rs +++ b/src/use_document_visibility.rs @@ -1,4 +1,7 @@ +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] + use crate::use_event_listener; +use cfg_if::cfg_if; use leptos::ev::visibilitychange; use leptos::*; @@ -21,12 +24,24 @@ use leptos::*; /// # view! { cx, } /// # } /// ``` +/// +/// ## Server-Side Rendering +/// +/// On the server this returns a `Signal` that always contains the value `web_sys::VisibilityState::Hidden`. pub fn use_document_visibility(cx: Scope) -> Signal<web_sys::VisibilityState> { - let (visibility, set_visibility) = create_signal(cx, document().visibility_state()); + cfg_if! { if #[cfg(feature = "ssr")] { + let inital_visibility = web_sys::VisibilityState::Hidden; + } else { + let inital_visibility = document().visibility_state(); + }} - let _ = use_event_listener(cx, document(), visibilitychange, move |_| { - set_visibility.set(document().visibility_state()); - }); + let (visibility, set_visibility) = create_signal(cx, inital_visibility); + + cfg_if! { if #[cfg(not(feature = "ssr"))] { + let _ = use_event_listener(cx, document(), visibilitychange, move |_| { + set_visibility.set(document().visibility_state()); + }); + }} visibility.into() } diff --git a/src/use_element_hover.rs b/src/use_element_hover.rs index 349b8ca..a363cdb 100644 --- a/src/use_element_hover.rs +++ b/src/use_element_hover.rs @@ -30,7 +30,10 @@ use web_sys::AddEventListenerOptions; /// } /// # } /// ``` - +/// +/// ## Server-Side Rendering +/// +/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) pub fn use_element_hover<El, T>(cx: Scope, el: El) -> Signal<bool> where El: Clone, diff --git a/src/use_element_size.rs b/src/use_element_size.rs index 7ca03a8..4b429ad 100644 --- a/src/use_element_size.rs +++ b/src/use_element_size.rs @@ -39,6 +39,10 @@ use wasm_bindgen::JsCast; /// # } /// ``` /// +/// ## Server-Side Rendering +/// +/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) +/// /// ## See also /// /// - [`use_resize_observer`] diff --git a/src/use_element_visibility.rs b/src/use_element_visibility.rs index 7e576ea..8aa4d6e 100644 --- a/src/use_element_visibility.rs +++ b/src/use_element_visibility.rs @@ -31,6 +31,10 @@ use std::marker::PhantomData; /// # } /// ``` /// +/// ## Server-Side Rendering +/// +/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) +/// /// ## See also /// /// * [`use_intersection_observer`] diff --git a/src/use_event_listener.rs b/src/use_event_listener.rs index 44effb1..c4b0746 100644 --- a/src/use_event_listener.rs +++ b/src/use_event_listener.rs @@ -76,11 +76,9 @@ use wasm_bindgen::JsCast; /// # } /// ``` /// -/// Note if your components also run in SSR (Server Side Rendering), you might get errors -/// because DOM APIs like document and window are not available outside of the browser. -/// To avoid that you can put the logic inside a -/// [`create_effect`](https://docs.rs/leptos/latest/leptos/fn.create_effect.html) hook -/// which only runs client side. +/// ## Server-Side Rendering +/// +/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) pub fn use_event_listener<Ev, El, T, F>( cx: Scope, target: El, diff --git a/src/use_favicon.rs b/src/use_favicon.rs index 3ba0d96..18c2b47 100644 --- a/src/use_favicon.rs +++ b/src/use_favicon.rs @@ -1,4 +1,7 @@ +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] + use crate::watch; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::*; use wasm_bindgen::JsCast; @@ -53,6 +56,9 @@ use wasm_bindgen::JsCast; /// # } /// ``` /// +/// ## Server-Side Rendering +/// +/// On the server only the signals work but no favicon will be changed obviously. pub fn use_favicon(cx: Scope) -> (ReadSignal<Option<String>>, WriteSignal<Option<String>>) { use_favicon_with_options(cx, UseFaviconOptions::default()) } @@ -68,39 +74,41 @@ pub fn use_favicon_with_options( rel, } = options; - let link_selector = format!("link[rel*=\"{rel}\"]"); - let (favicon, set_favicon) = create_signal(cx, new_icon.get_untracked()); if matches!(&new_icon, MaybeSignal::Dynamic(_)) { create_effect(cx, move |_| set_favicon.set(new_icon.get())); } - let apply_icon = move |icon: &String| { - if let Some(head) = document().head() { - if let Ok(links) = head.query_selector_all(&link_selector) { - let href = format!("{base_url}{icon}"); + cfg_if! { if #[cfg(not(feature = "ssr"))] { + let link_selector = format!("link[rel*=\"{rel}\"]"); - for i in 0..links.length() { - let node = links.get(i).expect("checked length"); - let link: web_sys::HtmlLinkElement = node.unchecked_into(); - link.set_href(&href); + let apply_icon = move |icon: &String| { + if let Some(head) = document().head() { + if let Ok(links) = head.query_selector_all(&link_selector) { + let href = format!("{base_url}{icon}"); + + for i in 0..links.length() { + let node = links.get(i).expect("checked length"); + let link: web_sys::HtmlLinkElement = node.unchecked_into(); + link.set_href(&href); + } } } - } - }; + }; - let _ = watch( - cx, - move || favicon.get(), - move |new_icon, prev_icon, _| { - if Some(new_icon) != prev_icon { - if let Some(new_icon) = new_icon { - apply_icon(new_icon); + let _ = watch( + cx, + move || favicon.get(), + move |new_icon, prev_icon, _| { + if Some(new_icon) != prev_icon { + if let Some(new_icon) = new_icon { + apply_icon(new_icon); + } } - } - }, - ); + }, + ); + }} (favicon, set_favicon) } diff --git a/src/use_intersection_observer.rs b/src/use_intersection_observer.rs index cbef635..b43581f 100644 --- a/src/use_intersection_observer.rs +++ b/src/use_intersection_observer.rs @@ -43,6 +43,10 @@ use wasm_bindgen::prelude::*; /// # } /// ``` /// +/// ## Server-Side Rendering +/// +/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) +/// /// ## See also /// /// * [`use_element_visibility`] diff --git a/src/use_interval.rs b/src/use_interval.rs index dbcba47..27b49b0 100644 --- a/src/use_interval.rs +++ b/src/use_interval.rs @@ -28,6 +28,10 @@ use leptos::*; /// # view! { cx, } /// # } /// ``` +/// +/// ## Server-Side Rendering +/// +/// On the server this function will simply be ignored. pub fn use_interval<N>( cx: Scope, interval: N, diff --git a/src/use_interval_fn.rs b/src/use_interval_fn.rs index 1e36e4e..2c03bf8 100644 --- a/src/use_interval_fn.rs +++ b/src/use_interval_fn.rs @@ -1,5 +1,8 @@ +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] + use crate::utils::Pausable; use crate::watch; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::leptos_dom::helpers::IntervalHandle; use leptos::*; @@ -32,6 +35,10 @@ use std::time::Duration; /// # view! { cx, } /// # } /// ``` +/// +/// ## Server-Side Rendering +/// +/// On the server this function will simply be ignored. pub fn use_interval_fn<CbFn, N>( cx: Scope, callback: CbFn, @@ -86,21 +93,23 @@ where let interval = interval.into(); let resume = move || { - let interval_value = interval.get(); - if interval_value == 0 { - return; - } + cfg_if! { if #[cfg(not(feature = "ssr"))] { + let interval_value = interval.get(); + if interval_value == 0 { + return; + } - set_active.set(true); + set_active.set(true); - if immediate_callback { - callback.clone()(); - } - clean(); + if immediate_callback { + callback.clone()(); + } + clean(); - timer.set( - set_interval_with_handle(callback.clone(), Duration::from_millis(interval_value)).ok(), - ); + timer.set( + set_interval_with_handle(callback.clone(), Duration::from_millis(interval_value)).ok(), + ); + }} }; if immediate { diff --git a/src/use_intl_number_format.rs b/src/use_intl_number_format.rs index 73f4b01..94bd69e 100644 --- a/src/use_intl_number_format.rs +++ b/src/use_intl_number_format.rs @@ -1,4 +1,7 @@ +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports, dead_code))] + use crate::utils::js_value_from_to_string; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use js_sys::Reflect; use leptos::*; @@ -31,7 +34,7 @@ use wasm_bindgen::{JsCast, JsValue}; /// # } /// ``` /// -/// ### Using locales +/// ## Using locales /// /// This example shows some of the variations in localized number formats. In order to get the format /// of the language used in the user interface of your application, make sure to specify that language @@ -81,7 +84,7 @@ use wasm_bindgen::{JsCast, JsValue}; /// # } /// ``` /// -/// ### Using options +/// ## Using options /// /// The results can be customized in multiple ways. /// @@ -143,20 +146,29 @@ use wasm_bindgen::{JsCast, JsValue}; /// /// For an exhaustive list of options see [`UseIntlNumberFormatOptions`](https://docs.rs/leptos_use/latest/leptos_use/struct.UseIntlNumberFormatOptions.html). /// -/// ### Formatting ranges +/// ## Formatting ranges /// /// Apart from the `format` method, the `format_range` method can be used to format a range of numbers. /// Please see [`UseIntlNumberFormatReturn::format_range`](https://docs.rs/leptos_use/latest/leptos_use/struct.UseIntlNumberFormatReturn.html#method.format_range) /// for details. +/// +/// ## Server-Side Rendering +/// +/// Since `Intl.NumberFormat` is a JavaScript API it is not available on the server. That's why +/// it falls back to a simple call to `format!()` on the server. pub fn use_intl_number_format(options: UseIntlNumberFormatOptions) -> UseIntlNumberFormatReturn { - let number_format = js_sys::Intl::NumberFormat::new( - &js_sys::Array::from_iter(options.locales.iter().map(JsValue::from)), - &js_sys::Object::from(options), - ); + cfg_if! { if #[cfg(feature = "ssr")] { + UseIntlNumberFormatReturn + } else { + let number_format = js_sys::Intl::NumberFormat::new( + &js_sys::Array::from_iter(options.locales.iter().map(JsValue::from)), + &js_sys::Object::from(options), + ); - UseIntlNumberFormatReturn { - js_intl_number_format: number_format, - } + UseIntlNumberFormatReturn { + js_intl_number_format: number_format, + } + }} } #[derive(Default, Copy, Clone, PartialEq, Eq, Hash, Debug)] @@ -788,33 +800,45 @@ impl From<UseIntlNumberFormatOptions> for js_sys::Object { } } -/// Return type of [`use_intl_number_format`]. -pub struct UseIntlNumberFormatReturn { - /// The instance of [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat). - pub js_intl_number_format: js_sys::Intl::NumberFormat, -} +cfg_if! { if #[cfg(feature = "ssr")] { + /// Return type of [`use_intl_number_format`]. + pub struct UseIntlNumberFormatReturn; +} else { + /// Return type of [`use_intl_number_format`]. + pub struct UseIntlNumberFormatReturn { + /// The instance of [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat). + pub js_intl_number_format: js_sys::Intl::NumberFormat, + } +}} impl UseIntlNumberFormatReturn { /// Formats a number according to the [locale and formatting options](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#parameters) of this `Intl.NumberFormat` object. /// See [`use_intl_number_format`] for more information. pub fn format<N>(&self, cx: Scope, number: impl Into<MaybeSignal<N>>) -> Signal<String> where - N: Clone + 'static, + N: Clone + Display + 'static, js_sys::Number: From<N>, { let number = number.into(); - let number_format = self.js_intl_number_format.clone(); - Signal::derive(cx, move || { - if let Ok(result) = number_format - .format() - .call1(&number_format, &js_sys::Number::from(number.get()).into()) - { - result.as_string().unwrap_or_default() - } else { - "".to_string() - } - }) + cfg_if! { if #[cfg(feature = "ssr")] { + Signal::derive(cx, move || { + format!("{}", number.get()) + }) + } else { + let number_format = self.js_intl_number_format.clone(); + + Signal::derive(cx, move || { + if let Ok(result) = number_format + .format() + .call1(&number_format, &js_sys::Number::from(number.get()).into()) + { + result.as_string().unwrap_or_default() + } else { + "".to_string() + } + }) + }} } /// Formats a range of numbers according to the locale and formatting options of this `Intl.NumberFormat` object. @@ -870,30 +894,37 @@ impl UseIntlNumberFormatReturn { end: impl Into<MaybeSignal<NEnd>>, ) -> Signal<String> where - NStart: Clone + 'static, - NEnd: Clone + 'static, + NStart: Clone + Display + 'static, + NEnd: Clone + Display + 'static, js_sys::Number: From<NStart>, js_sys::Number: From<NEnd>, { let start = start.into(); let end = end.into(); - let number_format = self.js_intl_number_format.clone(); - Signal::derive(cx, move || { - if let Ok(function) = Reflect::get(&number_format, &"formatRange".into()) { - let function = function.unchecked_into::<js_sys::Function>(); + cfg_if! { if #[cfg(feature = "ssr")] { + Signal::derive(cx, move || { + format!("{} - {}", start.get(), end.get()) + }) + } else { + let number_format = self.js_intl_number_format.clone(); - if let Ok(result) = function.call2( - &number_format, - &js_sys::Number::from(start.get()).into(), - &js_sys::Number::from(end.get()).into(), - ) { - return result.as_string().unwrap_or_default(); + Signal::derive(cx, move || { + if let Ok(function) = Reflect::get(&number_format, &"formatRange".into()) { + let function = function.unchecked_into::<js_sys::Function>(); + + if let Ok(result) = function.call2( + &number_format, + &js_sys::Number::from(start.get()).into(), + &js_sys::Number::from(end.get()).into(), + ) { + return result.as_string().unwrap_or_default(); + } } - } - "".to_string() - }) + "".to_string() + }) + }} } // TODO : Allows locale-aware formatting of strings produced by this `Intl.NumberFormat` object. diff --git a/src/use_media_query.rs b/src/use_media_query.rs index 137b87d..88beeb3 100644 --- a/src/use_media_query.rs +++ b/src/use_media_query.rs @@ -1,5 +1,8 @@ +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports, dead_code))] + use crate::use_event_listener; use crate::utils::CloneableFnMutWithArg; +use cfg_if::cfg_if; use leptos::ev::change; use leptos::*; use std::cell::RefCell; @@ -28,6 +31,10 @@ use std::rc::Rc; /// # } /// ``` /// +/// ## Server-Side Rendering +/// +/// On the server this functions returns a Signal that is always `false`. +/// /// ## See also /// /// * [`use_preferred_dark`] @@ -37,56 +44,58 @@ pub fn use_media_query(cx: Scope, query: impl Into<MaybeSignal<String>>) -> Sign let (matches, set_matches) = create_signal(cx, false); - let media_query: Rc<RefCell<Option<web_sys::MediaQueryList>>> = Rc::new(RefCell::new(None)); - let remove_listener: RemoveListener = Rc::new(RefCell::new(None)); + cfg_if! { if #[cfg(not(feature = "ssr"))] { + let media_query: Rc<RefCell<Option<web_sys::MediaQueryList>>> = Rc::new(RefCell::new(None)); + let remove_listener: RemoveListener = Rc::new(RefCell::new(None)); - let listener: Rc<RefCell<Box<dyn CloneableFnMutWithArg<web_sys::Event>>>> = - Rc::new(RefCell::new(Box::new(|_| {}))); + let listener: Rc<RefCell<Box<dyn CloneableFnMutWithArg<web_sys::Event>>>> = + Rc::new(RefCell::new(Box::new(|_| {}))); - let cleanup = { - let remove_listener = Rc::clone(&remove_listener); + let cleanup = { + let remove_listener = Rc::clone(&remove_listener); - move || { - if let Some(remove_listener) = remove_listener.take().as_ref() { - remove_listener(); + move || { + if let Some(remove_listener) = remove_listener.take().as_ref() { + remove_listener(); + } } - } - }; + }; - let update = { - let cleanup = cleanup.clone(); - let listener = Rc::clone(&listener); + let update = { + let cleanup = cleanup.clone(); + let listener = Rc::clone(&listener); - move || { - cleanup(); + move || { + cleanup(); - let mut media_query = media_query.borrow_mut(); - *media_query = window().match_media(&query.get()).unwrap_or(None); + let mut media_query = media_query.borrow_mut(); + *media_query = window().match_media(&query.get()).unwrap_or(None); - if let Some(media_query) = media_query.as_ref() { - set_matches.set(media_query.matches()); + if let Some(media_query) = media_query.as_ref() { + set_matches.set(media_query.matches()); - remove_listener.replace(Some(Box::new(use_event_listener( - cx, - media_query.clone(), - change, - listener.borrow().clone(), - )))); - } else { - set_matches.set(false); + remove_listener.replace(Some(Box::new(use_event_listener( + cx, + media_query.clone(), + change, + listener.borrow().clone(), + )))); + } else { + set_matches.set(false); + } } + }; + + { + let update = update.clone(); + listener + .replace(Box::new(move |_| update()) as Box<dyn CloneableFnMutWithArg<web_sys::Event>>); } - }; - { - let update = update.clone(); - listener - .replace(Box::new(move |_| update()) as Box<dyn CloneableFnMutWithArg<web_sys::Event>>); - } + create_effect(cx, move |_| update()); - create_effect(cx, move |_| update()); - - on_cleanup(cx, cleanup); + on_cleanup(cx, cleanup); + }} matches.into() } diff --git a/src/use_mouse.rs b/src/use_mouse.rs index bade31b..b9d3833 100644 --- a/src/use_mouse.rs +++ b/src/use_mouse.rs @@ -1,5 +1,8 @@ +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] + use crate::core::{ElementMaybeSignal, Position}; use crate::use_event_listener_with_options; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::ev::{dragover, mousemove, touchend, touchmove, touchstart}; use leptos::*; @@ -83,6 +86,10 @@ use web_sys::AddEventListenerOptions; /// view! { cx, <div node_ref=element></div> } /// } /// ``` +/// +/// ## Server-Side Rendering +/// +/// On the server this returns simple `Signal`s with the `initial_value`s. pub fn use_mouse(cx: Scope) -> UseMouseReturn { use_mouse_with_options(cx, Default::default()) } @@ -154,49 +161,51 @@ where // TODO : event filters? - let target = options.target; - let mut event_listener_options = AddEventListenerOptions::new(); - event_listener_options.passive(true); + cfg_if! { if #[cfg(not(feature = "ssr"))] { + let target = options.target; + let mut event_listener_options = AddEventListenerOptions::new(); + event_listener_options.passive(true); - let _ = use_event_listener_with_options( - cx, - target.clone(), - mousemove, - mouse_handler, - event_listener_options.clone(), - ); - let _ = use_event_listener_with_options( - cx, - target.clone(), - dragover, - drag_handler, - event_listener_options.clone(), - ); - if options.touch && !matches!(options.coord_type, UseMouseCoordType::Movement) { let _ = use_event_listener_with_options( cx, target.clone(), - touchstart, - touch_handler.clone(), + mousemove, + mouse_handler, event_listener_options.clone(), ); let _ = use_event_listener_with_options( cx, target.clone(), - touchmove, - touch_handler, + dragover, + drag_handler, event_listener_options.clone(), ); - if options.reset_on_touch_ends { + if options.touch && !matches!(options.coord_type, UseMouseCoordType::Movement) { let _ = use_event_listener_with_options( cx, - target, - touchend, - move |_| reset(), + target.clone(), + touchstart, + touch_handler.clone(), event_listener_options.clone(), ); + let _ = use_event_listener_with_options( + cx, + target.clone(), + touchmove, + touch_handler, + event_listener_options.clone(), + ); + if options.reset_on_touch_ends { + let _ = use_event_listener_with_options( + cx, + target, + touchend, + move |_| reset(), + event_listener_options.clone(), + ); + } } - } + }} UseMouseReturn { x, @@ -235,18 +244,33 @@ where _marker: PhantomData<T>, } -impl Default for UseMouseOptions<web_sys::Window, web_sys::Window, UseMouseEventExtractorDefault> { - fn default() -> Self { - Self { - coord_type: UseMouseCoordType::<UseMouseEventExtractorDefault>::default(), - target: window(), - touch: true, - reset_on_touch_ends: false, - initial_value: Position { x: 0.0, y: 0.0 }, - _marker: Default::default(), +cfg_if! { if #[cfg(feature = "ssr")] { + impl Default for UseMouseOptions<Option<web_sys::Window>, web_sys::Window, UseMouseEventExtractorDefault> { + fn default() -> Self { + Self { + coord_type: UseMouseCoordType::<UseMouseEventExtractorDefault>::default(), + target: None, + touch: true, + reset_on_touch_ends: false, + initial_value: Position { x: 0.0, y: 0.0 }, + _marker: Default::default(), + } } } -} +} else { + impl Default for UseMouseOptions<web_sys::Window, web_sys::Window, UseMouseEventExtractorDefault> { + fn default() -> Self { + Self { + coord_type: UseMouseCoordType::<UseMouseEventExtractorDefault>::default(), + target: window(), + touch: true, + reset_on_touch_ends: false, + initial_value: Position { x: 0.0, y: 0.0 }, + _marker: Default::default(), + } + } + } +}} /// Defines how to get the coordinates from the event. #[derive(Clone)] diff --git a/src/use_mutation_observer.rs b/src/use_mutation_observer.rs index 46f42ab..3085f9f 100644 --- a/src/use_mutation_observer.rs +++ b/src/use_mutation_observer.rs @@ -45,6 +45,10 @@ use web_sys::MutationObserverInit; /// } /// # } /// ``` +/// +/// ## Server-Side Rendering +/// +/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) pub fn use_mutation_observer<El, T, F>( cx: Scope, target: El, diff --git a/src/use_preferred_contrast.rs b/src/use_preferred_contrast.rs index 950f407..7255282 100644 --- a/src/use_preferred_contrast.rs +++ b/src/use_preferred_contrast.rs @@ -19,6 +19,10 @@ use std::fmt::Display; /// # } /// ``` /// +/// ## Server-Side Rendering +/// +/// On the server this returns a `Signal` that always contains the value `PreferredContrast::NoPreference`. +/// /// ## See also /// /// * [`use_media_query`] diff --git a/src/use_preferred_dark.rs b/src/use_preferred_dark.rs index d5032e8..ab56b8c 100644 --- a/src/use_preferred_dark.rs +++ b/src/use_preferred_dark.rs @@ -18,6 +18,10 @@ use leptos::*; /// # } /// ``` /// +/// ## Server-Side Rendering +/// +/// On the server this functions returns a Signal that is always `false`. +/// /// ## See also /// /// * [`use_media_query`] diff --git a/src/use_resize_observer.rs b/src/use_resize_observer.rs index cb96f7b..5a8de9a 100644 --- a/src/use_resize_observer.rs +++ b/src/use_resize_observer.rs @@ -43,6 +43,11 @@ use wasm_bindgen::prelude::*; /// } /// # } /// ``` +/// +/// ## Server-Side Rendering +/// +/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) +/// /// ## See also /// /// - [`use_element_size`] diff --git a/src/use_scroll.rs b/src/use_scroll.rs index dc0f2c3..6cc5aa9 100644 --- a/src/use_scroll.rs +++ b/src/use_scroll.rs @@ -164,6 +164,10 @@ use wasm_bindgen::JsCast; /// # } /// # } /// ``` +/// +/// ## Server-Side Rendering +/// +/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) pub fn use_scroll<El, T>(cx: Scope, element: El) -> UseScrollReturn where El: Clone, diff --git a/src/use_throttle_fn.rs b/src/use_throttle_fn.rs index b0c4aa6..a3bf15f 100644 --- a/src/use_throttle_fn.rs +++ b/src/use_throttle_fn.rs @@ -63,6 +63,11 @@ pub use crate::utils::ThrottleOptions; /// /// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle) /// - [Debouncing and Throttling Explained Through Examples](https://css-tricks.com/debouncing-throttling-explained-examples/) +/// +/// ## Server-Side Rendering +/// +/// Internally this uses `setTimeout` which is not supported on the server. So usually calling +/// a throttled function on the server will simply be ignored. pub fn use_throttle_fn<F, R>( func: F, ms: impl Into<MaybeSignal<f64>> + 'static, diff --git a/src/use_websocket.rs b/src/use_websocket.rs index 9dd8e20..a9524a0 100644 --- a/src/use_websocket.rs +++ b/src/use_websocket.rs @@ -1,6 +1,8 @@ -use leptos::{leptos_dom::helpers::TimeoutHandle, *}; +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports, dead_code))] +use cfg_if::cfg_if; use core::fmt; +use leptos::{leptos_dom::helpers::TimeoutHandle, *}; use std::rc::Rc; use std::time::Duration; @@ -21,7 +23,7 @@ use crate::utils::CloneableFnWithArg; /// /// ``` /// # use leptos::*; -/// # use leptos_use::websocket::*; +/// # use leptos_use::{use_websocket, UseWebSocketReadyState, UseWebsocketReturn}; /// # /// # #[component] /// # fn Demo(cx: Scope) -> impl IntoView { @@ -73,7 +75,10 @@ use crate::utils::CloneableFnWithArg; /// } /// # } /// ``` -// #[doc(cfg(feature = "websocket"))] +/// +/// ## Server-Side Rendering +/// +/// On the server the returned functions amount to noops. pub fn use_websocket( cx: Scope, url: String, @@ -87,7 +92,6 @@ pub fn use_websocket( } /// Version of [`use_websocket`] that takes `UseWebSocketOptions`. See [`use_websocket`] for how to use. -// #[doc(cfg(feature = "websocket"))] pub fn use_websocket_with_options( cx: Scope, url: String, @@ -103,173 +107,176 @@ pub fn use_websocket_with_options( let (message_bytes, set_message_bytes) = create_signal(cx, None); let ws_ref: StoredValue<Option<WebSocket>> = store_value(cx, None); - let on_open_ref = store_value(cx, options.on_open); - let on_message_ref = store_value(cx, options.on_message); - let on_message_bytes_ref = store_value(cx, options.on_message_bytes); - let on_error_ref = store_value(cx, options.on_error); - let on_close_ref = store_value(cx, options.on_close); - let reconnect_limit = options.reconnect_limit.unwrap_or(3); - let reconnect_interval = options.reconnect_interval.unwrap_or(3 * 1000); let reconnect_timer_ref: StoredValue<Option<TimeoutHandle>> = store_value(cx, None); let manual = options.manual; - let protocols = options.protocols; let reconnect_times_ref: StoredValue<u64> = store_value(cx, 0); let unmounted_ref = store_value(cx, false); let connect_ref: StoredValue<Option<Rc<dyn Fn()>>> = store_value(cx, None); - let reconnect_ref: StoredValue<Option<Rc<dyn Fn()>>> = store_value(cx, None); - reconnect_ref.set_value({ - let ws = ws_ref.get_value(); - Some(Rc::new(move || { - if reconnect_times_ref.get_value() < reconnect_limit - && ws + cfg_if! { if #[cfg(not(feature = "ssr"))] { + let on_open_ref = store_value(cx, options.on_open); + let on_message_ref = store_value(cx, options.on_message); + let on_message_bytes_ref = store_value(cx, options.on_message_bytes); + let on_error_ref = store_value(cx, options.on_error); + let on_close_ref = store_value(cx, options.on_close); + + let reconnect_interval = options.reconnect_interval.unwrap_or(3 * 1000); + let protocols = options.protocols; + + let reconnect_ref: StoredValue<Option<Rc<dyn Fn()>>> = store_value(cx, None); + reconnect_ref.set_value({ + let ws = ws_ref.get_value(); + Some(Rc::new(move || { + if reconnect_times_ref.get_value() < reconnect_limit + && ws .clone() .map_or(false, |ws: WebSocket| ws.ready_state() != WebSocket::OPEN) - { - reconnect_timer_ref.set_value( - set_timeout_with_handle( - move || { - if let Some(connect) = connect_ref.get_value() { - connect(); - reconnect_times_ref.update_value(|current| *current += 1); - } - }, - Duration::from_millis(reconnect_interval), - ) - .ok(), - ); - } - })) - }); - - connect_ref.set_value({ - let ws = ws_ref.get_value(); - let url = url; - - Some(Rc::new(move || { - reconnect_timer_ref.set_value(None); - { - if let Some(web_socket) = &ws { - let _ = web_socket.close(); - } - } - - let web_socket = { - protocols.as_ref().map_or_else( - || WebSocket::new(&url).unwrap_throw(), - |protocols| { - let array = protocols - .iter() - .map(|p| JsValue::from(p.clone())) - .collect::<Array>(); - WebSocket::new_with_str_sequence(&url, &JsValue::from(&array)) - .unwrap_throw() - }, - ) - }; - web_socket.set_binary_type(BinaryType::Arraybuffer); - set_ready_state.set(UseWebSocketReadyState::Connecting); - - // onopen handler - { - let onopen_closure = Closure::wrap(Box::new(move |e: Event| { - if unmounted_ref.get_value() { - return; - } - - let callback = on_open_ref.get_value(); - callback(e); - - set_ready_state.set(UseWebSocketReadyState::Open); - }) as Box<dyn FnMut(Event)>); - web_socket.set_onopen(Some(onopen_closure.as_ref().unchecked_ref())); - // Forget the closure to keep it alive - onopen_closure.forget(); - } - - // onmessage handler - { - let onmessage_closure = Closure::wrap(Box::new(move |e: MessageEvent| { - if unmounted_ref.get_value() { - return; - } - - e.data().dyn_into::<js_sys::ArrayBuffer>().map_or_else( - |_| { - e.data().dyn_into::<js_sys::JsString>().map_or_else( - |_| { - unreachable!("message event, received Unknown: {:?}", e.data()); - }, - |txt| { - let txt = String::from(&txt); - let callback = on_message_ref.get_value(); - callback(txt.clone()); - - set_message.set(Some(txt)); - }, - ); - }, - |array_buffer| { - let array = js_sys::Uint8Array::new(&array_buffer); - let array = array.to_vec(); - let callback = on_message_bytes_ref.get_value(); - callback(array.clone()); - - set_message_bytes.set(Some(array)); - }, + { + reconnect_timer_ref.set_value( + set_timeout_with_handle( + move || { + if let Some(connect) = connect_ref.get_value() { + connect(); + reconnect_times_ref.update_value(|current| *current += 1); + } + }, + Duration::from_millis(reconnect_interval), + ) + .ok(), ); - }) - as Box<dyn FnMut(MessageEvent)>); - web_socket.set_onmessage(Some(onmessage_closure.as_ref().unchecked_ref())); - onmessage_closure.forget(); - } - // onerror handler - { - let onerror_closure = Closure::wrap(Box::new(move |e: Event| { - if unmounted_ref.get_value() { - return; + } + })) + }); + + connect_ref.set_value({ + let ws = ws_ref.get_value(); + let url = url; + + Some(Rc::new(move || { + reconnect_timer_ref.set_value(None); + { + if let Some(web_socket) = &ws { + let _ = web_socket.close(); } + } - if let Some(reconnect) = &reconnect_ref.get_value() { - reconnect(); - } + let web_socket = { + protocols.as_ref().map_or_else( + || WebSocket::new(&url).unwrap_throw(), + |protocols| { + let array = protocols + .iter() + .map(|p| JsValue::from(p.clone())) + .collect::<Array>(); + WebSocket::new_with_str_sequence(&url, &JsValue::from(&array)) + .unwrap_throw() + }, + ) + }; + web_socket.set_binary_type(BinaryType::Arraybuffer); + set_ready_state.set(UseWebSocketReadyState::Connecting); - let callback = on_error_ref.get_value(); - callback(e); + // onopen handler + { + let onopen_closure = Closure::wrap(Box::new(move |e: Event| { + if unmounted_ref.get_value() { + return; + } - set_ready_state.set(UseWebSocketReadyState::Closed); - }) as Box<dyn FnMut(Event)>); - web_socket.set_onerror(Some(onerror_closure.as_ref().unchecked_ref())); - onerror_closure.forget(); - } - // onclose handler - { - let onclose_closure = Closure::wrap(Box::new(move |e: CloseEvent| { - if unmounted_ref.get_value() { - return; - } + let callback = on_open_ref.get_value(); + callback(e); - if let Some(reconnect) = &reconnect_ref.get_value() { - reconnect(); - } + set_ready_state.set(UseWebSocketReadyState::Open); + }) as Box<dyn FnMut(Event)>); + web_socket.set_onopen(Some(onopen_closure.as_ref().unchecked_ref())); + // Forget the closure to keep it alive + onopen_closure.forget(); + } - let callback = on_close_ref.get_value(); - callback(e); + // onmessage handler + { + let onmessage_closure = Closure::wrap(Box::new(move |e: MessageEvent| { + if unmounted_ref.get_value() { + return; + } - set_ready_state.set(UseWebSocketReadyState::Closed); - }) - as Box<dyn FnMut(CloseEvent)>); - web_socket.set_onclose(Some(onclose_closure.as_ref().unchecked_ref())); - onclose_closure.forget(); - } + e.data().dyn_into::<js_sys::ArrayBuffer>().map_or_else( + |_| { + e.data().dyn_into::<js_sys::JsString>().map_or_else( + |_| { + unreachable!("message event, received Unknown: {:?}", e.data()); + }, + |txt| { + let txt = String::from(&txt); + let callback = on_message_ref.get_value(); + callback(txt.clone()); - ws_ref.set_value(Some(web_socket)); - })) - }); + set_message.set(Some(txt)); + }, + ); + }, + |array_buffer| { + let array = js_sys::Uint8Array::new(&array_buffer); + let array = array.to_vec(); + let callback = on_message_bytes_ref.get_value(); + callback(array.clone()); + + set_message_bytes.set(Some(array)); + }, + ); + }) + as Box<dyn FnMut(MessageEvent)>); + web_socket.set_onmessage(Some(onmessage_closure.as_ref().unchecked_ref())); + onmessage_closure.forget(); + } + // onerror handler + { + let onerror_closure = Closure::wrap(Box::new(move |e: Event| { + if unmounted_ref.get_value() { + return; + } + + if let Some(reconnect) = &reconnect_ref.get_value() { + reconnect(); + } + + let callback = on_error_ref.get_value(); + callback(e); + + set_ready_state.set(UseWebSocketReadyState::Closed); + }) as Box<dyn FnMut(Event)>); + web_socket.set_onerror(Some(onerror_closure.as_ref().unchecked_ref())); + onerror_closure.forget(); + } + // onclose handler + { + let onclose_closure = Closure::wrap(Box::new(move |e: CloseEvent| { + if unmounted_ref.get_value() { + return; + } + + if let Some(reconnect) = &reconnect_ref.get_value() { + reconnect(); + } + + let callback = on_close_ref.get_value(); + callback(e); + + set_ready_state.set(UseWebSocketReadyState::Closed); + }) + as Box<dyn FnMut(CloseEvent)>); + web_socket.set_onclose(Some(onclose_closure.as_ref().unchecked_ref())); + onclose_closure.forget(); + } + + ws_ref.set_value(Some(web_socket)); + })) + }); + }} // Send text (String) let send = { @@ -283,29 +290,26 @@ pub fn use_websocket_with_options( }; // Send bytes - let send_bytes = { - move |data: Vec<u8>| { - if ready_state.get() == UseWebSocketReadyState::Open { - if let Some(web_socket) = ws_ref.get_value() { - let _ = web_socket.send_with_u8_array(&data); - } + let send_bytes = move |data: Vec<u8>| { + if ready_state.get() == UseWebSocketReadyState::Open { + if let Some(web_socket) = ws_ref.get_value() { + let _ = web_socket.send_with_u8_array(&data); } } }; // Open connection - let open = { - move || { - reconnect_times_ref.set_value(0); - if let Some(connect) = connect_ref.get_value() { - connect(); - } + let open = move || { + reconnect_times_ref.set_value(0); + if let Some(connect) = connect_ref.get_value() { + connect(); } }; // Close connection let close = { reconnect_timer_ref.set_value(None); + move || { reconnect_times_ref.set_value(reconnect_limit); if let Some(web_socket) = ws_ref.get_value() { @@ -319,8 +323,6 @@ pub fn use_websocket_with_options( if !manual { open(); } - - || () }); // clean up (unmount) diff --git a/src/use_window_focus.rs b/src/use_window_focus.rs index 93a449b..bc8749e 100644 --- a/src/use_window_focus.rs +++ b/src/use_window_focus.rs @@ -1,4 +1,7 @@ +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] + use crate::use_event_listener; +use cfg_if::cfg_if; use leptos::ev::{blur, focus}; use leptos::*; @@ -22,11 +25,23 @@ use leptos::*; /// # view! { cx, } /// # } /// ``` +/// +/// ## Server-Side Rendering +/// +/// On the server this returns a `Signal` that is always `true`. pub fn use_window_focus(cx: Scope) -> Signal<bool> { - let (focused, set_focused) = create_signal(cx, document().has_focus().unwrap_or_default()); + cfg_if! { if #[cfg(feature = "ssr")] { + let initial_focus = true; + } else { + let initial_focus = document().has_focus().unwrap_or_default(); + }} - let _ = use_event_listener(cx, window(), blur, move |_| set_focused.set(false)); - let _ = use_event_listener(cx, window(), focus, move |_| set_focused.set(true)); + let (focused, set_focused) = create_signal(cx, initial_focus); + + cfg_if! { if #[cfg(not(feature = "ssr"))] { + let _ = use_event_listener(cx, window(), blur, move |_| set_focused.set(false)); + let _ = use_event_listener(cx, window(), focus, move |_| set_focused.set(true)); + }} focused.into() } diff --git a/src/use_window_scroll.rs b/src/use_window_scroll.rs index c037c09..64a1606 100644 --- a/src/use_window_scroll.rs +++ b/src/use_window_scroll.rs @@ -1,4 +1,7 @@ +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] + use crate::use_event_listener_with_options; +use cfg_if::cfg_if; use leptos::ev::scroll; use leptos::*; use web_sys::AddEventListenerOptions; @@ -22,24 +25,37 @@ use web_sys::AddEventListenerOptions; /// # view! { cx, } /// # } /// ``` +/// +/// ## Server-Side Rendering +/// +/// On the server this returns `Signal`s that are always `0.0`. pub fn use_window_scroll(cx: Scope) -> (Signal<f64>, Signal<f64>) { - let (x, set_x) = create_signal(cx, window().scroll_x().unwrap_or_default()); - let (y, set_y) = create_signal(cx, window().scroll_y().unwrap_or_default()); + cfg_if! { if #[cfg(feature = "ssr")] { + let initial_x = 0.0; + let initial_y = 0.0; + } else { + let initial_x = window().scroll_x().unwrap_or_default(); + let initial_y = window().scroll_y().unwrap_or_default(); + }} + let (x, set_x) = create_signal(cx, initial_x); + let (y, set_y) = create_signal(cx, initial_y); - let mut options = AddEventListenerOptions::new(); - options.capture(false); - options.passive(true); + cfg_if! { if #[cfg(not(feature = "ssr"))] { + let mut options = AddEventListenerOptions::new(); + options.capture(false); + options.passive(true); - let _ = use_event_listener_with_options( - cx, - window(), - scroll, - move |_| { - set_x.set(window().scroll_x().unwrap_or_default()); - set_y.set(window().scroll_y().unwrap_or_default()); - }, - options, - ); + let _ = use_event_listener_with_options( + cx, + window(), + scroll, + move |_| { + set_x.set(window().scroll_x().unwrap_or_default()); + set_y.set(window().scroll_y().unwrap_or_default()); + }, + options, + ); + }} (x.into(), y.into()) } diff --git a/src/utils/filters/debounce.rs b/src/utils/filters/debounce.rs index d196600..b33901b 100644 --- a/src/utils/filters/debounce.rs +++ b/src/utils/filters/debounce.rs @@ -1,4 +1,7 @@ +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] + use crate::utils::CloneableFnWithReturn; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::leptos_dom::helpers::TimeoutHandle; use leptos::{set_timeout_with_handle, MaybeSignal, SignalGetUntracked}; @@ -56,37 +59,39 @@ where return Rc::clone(&last_return_value); } - // Create the max_timer. Clears the regular timer on invoke - if let Some(max_duration) = max_duration { - if max_timer.get().is_none() { - let timer = Rc::clone(&timer); - let invok = invoke.clone(); - max_timer.set( - set_timeout_with_handle( - move || { - clear_timeout(&timer); - invok(); - }, - Duration::from_millis(max_duration as u64), - ) - .ok(), - ); + cfg_if! { if #[cfg(not(feature = "ssr"))] { + // Create the max_timer. Clears the regular timer on invoke + if let Some(max_duration) = max_duration { + if max_timer.get().is_none() { + let timer = Rc::clone(&timer); + let invok = invoke.clone(); + max_timer.set( + set_timeout_with_handle( + move || { + clear_timeout(&timer); + invok(); + }, + Duration::from_millis(max_duration as u64), + ) + .ok(), + ); + } } - } - let max_timer = Rc::clone(&max_timer); + let max_timer = Rc::clone(&max_timer); - // Create the regular timer. Clears the max timer on invoke - timer.set( - set_timeout_with_handle( - move || { - clear_timeout(&max_timer); - invoke(); - }, - Duration::from_millis(duration as u64), - ) - .ok(), - ); + // Create the regular timer. Clears the max timer on invoke + timer.set( + set_timeout_with_handle( + move || { + clear_timeout(&max_timer); + invoke(); + }, + Duration::from_millis(duration as u64), + ) + .ok(), + ); + }} Rc::clone(&last_return_value) } diff --git a/src/utils/filters/throttle.rs b/src/utils/filters/throttle.rs index 1b50400..012c398 100644 --- a/src/utils/filters/throttle.rs +++ b/src/utils/filters/throttle.rs @@ -1,4 +1,7 @@ +#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] + use crate::utils::CloneableFnWithReturn; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use js_sys::Date; use leptos::leptos_dom::helpers::TimeoutHandle; @@ -72,34 +75,38 @@ where last_exec.set(Date::now()); invoke(); } else if options.trailing { - let last_exec = Rc::clone(&last_exec); - let is_leading = Rc::clone(&is_leading); - timer.set( - set_timeout_with_handle( - move || { - last_exec.set(Date::now()); - is_leading.set(true); - invoke(); - clear(); - }, - Duration::from_millis(max(0, (duration - elapsed) as u64)), - ) - .ok(), - ); + cfg_if! { if #[cfg(not(feature = "ssr"))] { + let last_exec = Rc::clone(&last_exec); + let is_leading = Rc::clone(&is_leading); + timer.set( + set_timeout_with_handle( + move || { + last_exec.set(Date::now()); + is_leading.set(true); + invoke(); + clear(); + }, + Duration::from_millis(max(0, (duration - elapsed) as u64)), + ) + .ok(), + ); + }} } - if !options.leading && timer.get().is_none() { - let is_leading = Rc::clone(&is_leading); - timer.set( - set_timeout_with_handle( - move || { - is_leading.set(true); - }, - Duration::from_millis(duration as u64), - ) - .ok(), - ); - } + cfg_if! { if #[cfg(not(feature = "ssr"))] { + if !options.leading && timer.get().is_none() { + let is_leading = Rc::clone(&is_leading); + timer.set( + set_timeout_with_handle( + move || { + is_leading.set(true); + }, + Duration::from_millis(duration as u64), + ) + .ok(), + ); + } + }} is_leading.set(false); diff --git a/src/watch.rs b/src/watch.rs index 680470c..7b017d6 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -108,6 +108,12 @@ use std::rc::Rc; /// # } /// ``` /// +/// ## Server-Side Rendering +/// +/// On the server this works just fine except if you throttle or debounce in which case the callback +/// will never be called except if you set `immediate` to `true` in which case the callback will be +/// called exactly once. +/// /// ## See also /// /// * [`watch_throttled`] diff --git a/src/watch_debounced.rs b/src/watch_debounced.rs index a836c55..9083319 100644 --- a/src/watch_debounced.rs +++ b/src/watch_debounced.rs @@ -60,6 +60,12 @@ use leptos::*; /// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle) /// - [Debouncing and Throttling Explained Through Examples](https://css-tricks.com/debouncing-throttling-explained-examples/) /// +/// ## Server-Side Rendering +/// +/// On the server the callback +/// will never be called except if you set `immediate` to `true` in which case the callback will be +/// called exactly once. +/// /// ## See also /// /// * [`watch`] diff --git a/src/watch_pausable.rs b/src/watch_pausable.rs index 84d87fb..254e3e2 100644 --- a/src/watch_pausable.rs +++ b/src/watch_pausable.rs @@ -44,6 +44,12 @@ use leptos::*; /// /// There's also [`watch_pausable_with_options`] which takes the same options as [`watch`]. /// +/// ## Server-Side Rendering +/// +/// On the server this works just fine except if you throttle or debounce in which case the callback +/// will never be called except if you set `immediate` to `true` in which case the callback will be +/// called exactly once. +/// /// ## See also /// /// * [`watch`] diff --git a/src/watch_throttled.rs b/src/watch_throttled.rs index 4e02172..8775721 100644 --- a/src/watch_throttled.rs +++ b/src/watch_throttled.rs @@ -60,6 +60,12 @@ use leptos::*; /// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle) /// - [Debouncing and Throttling Explained Through Examples](https://css-tricks.com/debouncing-throttling-explained-examples/) /// +/// ## Server-Side Rendering +/// +/// On the server the callback +/// will never be called except if you set `immediate` to `true` in which case the callback will be +/// called exactly once. +/// /// ## See also /// /// * [`watch`] diff --git a/src/whenever.rs b/src/whenever.rs index 13f0df8..194468e 100644 --- a/src/whenever.rs +++ b/src/whenever.rs @@ -76,6 +76,12 @@ use leptos::*; /// # view! { cx, } /// # } /// ``` +/// +/// ## Server-Side Rendering +/// +/// On the server this works just fine except if you throttle or debounce in which case the callback +/// will never be called except if you set `immediate` to `true` in which case the callback will be +/// called exactly once. pub fn whenever<T, DFn, CFn>(cx: Scope, source: DFn, callback: CFn) -> impl Fn() + Clone where DFn: Fn() -> bool + 'static,