added SSR feature

This commit is contained in:
Maccesch 2023-07-14 22:43:19 +01:00
parent e2dcc81958
commit bc94577ec6
58 changed files with 1475 additions and 719 deletions

View file

@ -28,7 +28,7 @@ jobs:
- name: Check formatting - name: Check formatting
run: cargo fmt --check run: cargo fmt --check
- name: Clippy - name: Clippy
run: cargo clippy --all-features --tests -- -D warnings run: cargo clippy --features storage,docs,math --tests -- -D warnings
- name: Run tests - name: Run tests
run: cargo test --all-features run: cargo test --all-features

View file

@ -23,6 +23,6 @@ jobs:
- name: Check formatting - name: Check formatting
run: cargo fmt --check run: cargo fmt --check
- name: Clippy - name: Clippy
run: cargo clippy --all-features --tests -- -D warnings run: cargo clippy --features storage,docs,math --tests -- -D warnings
- name: Run tests - name: Run tests
run: cargo test --all-features run: cargo test --all-features

2
.idea/leptos-use.iml generated
View file

@ -43,6 +43,7 @@
<sourceFolder url="file://$MODULE_DIR$/examples/use_document_visibility/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/use_document_visibility/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_window_focus/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/use_window_focus/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_window_scroll/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/use_window_scroll/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/ssr/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_intl_number_format/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/use_intl_number_format/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_websocket/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/use_websocket/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/testtest/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/testtest/src" isTestSource="false" />
@ -69,6 +70,7 @@
<excludeFolder url="file://$MODULE_DIR$/examples/use_breakpoints/target" /> <excludeFolder url="file://$MODULE_DIR$/examples/use_breakpoints/target" />
<excludeFolder url="file://$MODULE_DIR$/examples/use_element_visibility/target" /> <excludeFolder url="file://$MODULE_DIR$/examples/use_element_visibility/target" />
<excludeFolder url="file://$MODULE_DIR$/examples/use_intersection_observer/target" /> <excludeFolder url="file://$MODULE_DIR$/examples/use_intersection_observer/target" />
<excludeFolder url="file://$MODULE_DIR$/examples/ssr/target" />
<excludeFolder url="file://$MODULE_DIR$/testtest/target" /> <excludeFolder url="file://$MODULE_DIR$/testtest/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

View file

@ -22,6 +22,7 @@ serde = { version = "1", optional = true }
serde_json = { version = "1", optional = true } serde_json = { version = "1", optional = true }
paste = "1" paste = "1"
lazy_static = "1" lazy_static = "1"
cfg-if = "1"
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3" version = "0.3"
@ -66,6 +67,7 @@ features = [
docs = [] docs = []
math = ["num"] math = ["num"]
storage = ["serde", "serde_json", "web-sys/StorageEvent"] storage = ["serde", "serde_json", "web-sys/StorageEvent"]
ssr = []
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View file

@ -11,6 +11,7 @@
<p align="center"> <p align="center">
<a href="https://crates.io/crates/leptos-use"><img src="https://img.shields.io/crates/v/leptos-use.svg?label=&color=%232C1275" alt="Crates.io"/></a> <a href="https://crates.io/crates/leptos-use"><img src="https://img.shields.io/crates/v/leptos-use.svg?label=&color=%232C1275" alt="Crates.io"/></a>
<a href="https://leptos-use.rs/server_side_rendering.html"><img src="https://img.shields.io/badge/-SSR-%236a214b" alt="SSR"></a>
<a href="https://leptos-use.rs"><img src="https://img.shields.io/badge/-docs%20%26%20demos-%239A233F" alt="Docs & Demos"></a> <a href="https://leptos-use.rs"><img src="https://img.shields.io/badge/-docs%20%26%20demos-%239A233F" alt="Docs & Demos"></a>
<a href="https://leptos-use.rs"><img src="https://img.shields.io/badge/-41%20functions-%23EF3939" alt="41 Functions" /></a> <a href="https://leptos-use.rs"><img src="https://img.shields.io/badge/-41%20functions-%23EF3939" alt="41 Functions" /></a>
</p> </p>

View file

@ -3,6 +3,7 @@
[Introduction](introduction.md) [Introduction](introduction.md)
[Get Started](get_started.md) [Get Started](get_started.md)
[Functions](functions.md) [Functions](functions.md)
[Server-Side Rendering](server_side_rendering.md)
[Changelog](changelog.md) [Changelog](changelog.md)
# @Storage # @Storage

View file

@ -10,6 +10,7 @@
<p> <p>
<a href="https://crates.io/crates/leptos-use"><img src="https://img.shields.io/crates/v/leptos-use.svg?label=&color=%232C1275" alt="Crates.io"/></a> <a href="https://crates.io/crates/leptos-use"><img src="https://img.shields.io/crates/v/leptos-use.svg?label=&color=%232C1275" alt="Crates.io"/></a>
<a href="https://leptos-use.rs/server_side_rendering.html"><img src="https://img.shields.io/badge/-SSR-%236a214b" alt="SSR"></a>
<a href="./get_started.html"><img src="https://img.shields.io/badge/-docs%20%26%20demos-%239A233F" alt="Docs & Demos"></a> <a href="./get_started.html"><img src="https://img.shields.io/badge/-docs%20%26%20demos-%239A233F" alt="Docs & Demos"></a>
<a href="./functions.html"><img src="https://img.shields.io/badge/-41%20functions-%23EF3939" alt="41 Functions" /></a> <a href="./functions.html"><img src="https://img.shields.io/badge/-41%20functions-%23EF3939" alt="41 Functions" /></a>
</p> </p>

View file

@ -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 |_| {
// ...
})
},
);
```

View file

@ -1,6 +1,2 @@
[build] [build]
rustflags = ["--cfg=web_sys_unstable_apis", "--cfg=has_std"] rustflags = ["--cfg=web_sys_unstable_apis", "--cfg=has_std"]
[unstable]
build-std = ["std", "panic_abort", "core", "alloc"]
build-std-features = ["panic_immediate_abort"]

View file

@ -38,6 +38,10 @@ members = [
"watch_throttled", "watch_throttled",
] ]
exclude = [
"ssr"
]
[package] [package]
name = "leptos-use-examples" name = "leptos-use-examples"
version = "0.3.3" version = "0.3.3"

14
examples/ssr/.gitignore vendored Normal file
View file

@ -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/

100
examples/ssr/Cargo.toml Normal file
View file

@ -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 <site-root>/<site-pkg>/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

56
examples/ssr/README.md Normal file
View file

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

79
examples/ssr/src/app.rs Normal file
View file

@ -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,
<Stylesheet id="leptos" href="/pkg/start-axum.css"/>
<Title text="Leptos-Use SSR Example"/>
<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>
}
}

View file

@ -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>
}
}
/>
}
}

View file

@ -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}"),
)),
}
}
}}

21
examples/ssr/src/lib.rs Normal file
View file

@ -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/> }
});
}
}}

43
examples/ssr/src/main.rs Normal file
View file

@ -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
}

View file

@ -0,0 +1,4 @@
body {
font-family: sans-serif;
text-align: center;
}

View file

@ -1,6 +1,8 @@
// #![feature(doc_cfg)] // #![feature(doc_cfg)]
//! Collection of essential Leptos utilities inspired by SolidJS USE / VueUse //! Collection of essential Leptos utilities inspired by SolidJS USE / VueUse
use cfg_if::cfg_if;
pub mod core; pub mod core;
#[cfg(feature = "docs")] #[cfg(feature = "docs")]
pub mod docs; pub mod docs;
@ -10,15 +12,13 @@ pub mod math;
pub mod storage; pub mod storage;
pub mod utils; pub mod utils;
#[cfg(web_sys_unstable_apis)] cfg_if! { if #[cfg(web_sys_unstable_apis)] {
mod use_element_size; mod use_element_size;
#[cfg(web_sys_unstable_apis)]
mod use_resize_observer; mod use_resize_observer;
#[cfg(web_sys_unstable_apis)]
pub use use_element_size::*; pub use use_element_size::*;
#[cfg(web_sys_unstable_apis)]
pub use use_resize_observer::*; pub use use_resize_observer::*;
}}
mod on_click_outside; mod on_click_outside;
mod use_active_element; mod use_active_element;

View file

@ -45,6 +45,10 @@ static IOS_WORKAROUND: RwLock<bool> = RwLock::new(false);
/// which is **not** supported by IE 11, Edge 18 and below. /// which is **not** supported by IE 11, Edge 18 and below.
/// If you are targeting these browsers, we recommend you to include /// If you are targeting these browsers, we recommend you to include
/// [this code snippet](https://gist.github.com/sibbng/13e83b1dd1b733317ce0130ef07d4efd) on your project. /// [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 pub fn on_click_outside<El, T, F>(cx: Scope, target: El, handler: F) -> impl FnOnce() + Clone
where where
El: Clone, El: Clone,

View file

@ -1,8 +1,11 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports, dead_code))]
use crate::utils::{CloneableFn, CloneableFnWithArg, FilterOptions}; use crate::utils::{CloneableFn, CloneableFnWithArg, FilterOptions};
use crate::{ use crate::{
filter_builder_methods, use_event_listener, watch_pausable_with_options, DebounceOptions, filter_builder_methods, use_event_listener, watch_pausable_with_options, DebounceOptions,
ThrottleOptions, WatchOptions, WatchPausableReturn, ThrottleOptions, WatchOptions, WatchPausableReturn,
}; };
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use js_sys::Reflect; use js_sys::Reflect;
use leptos::*; 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. /// 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 /// ## See also
/// ///
/// * [`use_local_storage`] /// * [`use_local_storage`]
@ -188,6 +195,9 @@ where
let (data, set_data) = create_signal(cx, defaults.get_untracked()); let (data, set_data) = create_signal(cx, defaults.get_untracked());
cfg_if! { if #[cfg(feature = "ssr")] {
let remove: Box<dyn CloneableFn> = Box::new(|| {});
} else {
let storage = storage_type.into_storage(); let storage = storage_type.into_storage();
let remove: Box<dyn CloneableFn> = match storage { let remove: Box<dyn CloneableFn> = match storage {
@ -374,6 +384,7 @@ where
Box::new(move || {}) Box::new(move || {})
} }
}; };
}}
(data, set_data, move || remove.clone()()) (data, set_data, move || remove.clone()())
} }

View file

@ -1,4 +1,7 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::use_event_listener_with_options; use crate::use_event_listener_with_options;
use cfg_if::cfg_if;
use leptos::ev::{blur, focus}; use leptos::ev::{blur, focus};
use leptos::html::{AnyElement, ToHtmlElement}; use leptos::html::{AnyElement, ToHtmlElement};
use leptos::*; use leptos::*;
@ -27,16 +30,24 @@ use web_sys::AddEventListenerOptions;
/// # view! { cx, } /// # 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>>> { pub fn use_active_element(cx: Scope) -> Signal<Option<HtmlElement<AnyElement>>> {
cfg_if! { if #[cfg(feature = "ssr")] {
let get_active_element = || { None };
} else {
let get_active_element = move || { let get_active_element = move || {
document() document()
.active_element() .active_element()
.map(|el| el.to_leptos_element(cx)) .map(|el| el.to_leptos_element(cx))
}; };
}}
let (active_element, set_active_element) = create_signal(cx, get_active_element()); let (active_element, set_active_element) = create_signal(cx, get_active_element());
cfg_if! { if #[cfg(not(feature = "ssr"))] {
let mut listener_options = AddEventListenerOptions::new(); let mut listener_options = AddEventListenerOptions::new();
listener_options.capture(true); listener_options.capture(true);
@ -63,6 +74,7 @@ pub fn use_active_element(cx: Scope) -> Signal<Option<HtmlElement<AnyElement>>>
}, },
listener_options, listener_options,
); );
}}
active_element.into() active_element.into()
} }

View file

@ -103,6 +103,11 @@ use std::hash::Hash;
/// # view! { cx, } /// # 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>( pub fn use_breakpoints<K: Eq + Hash + Debug + Clone>(
cx: Scope, cx: Scope,
breakpoints: HashMap<K, u32>, breakpoints: HashMap<K, u32>,

View file

@ -8,6 +8,7 @@ use std::fmt::{Display, Formatter};
use crate::core::StorageType; use crate::core::StorageType;
use crate::use_preferred_dark; use crate::use_preferred_dark;
use crate::utils::CloneableFnWithArg; use crate::utils::CloneableFnWithArg;
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::*; use leptos::*;
use std::marker::PhantomData; 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 /// ## See also
/// ///
/// * [`use_dark`] /// * [`use_dark`]
@ -247,9 +252,9 @@ where
} }
} }
#[cfg(not(feature = "storage"))]
/// Color modes /// 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 { pub enum ColorMode {
#[default] #[default]
Auto, Auto,
@ -258,35 +263,7 @@ pub enum ColorMode {
Custom(String), Custom(String),
} }
#[cfg(not(feature = "storage"))] cfg_if! { if #[cfg(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(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( fn get_store_signal(
cx: Scope, cx: Scope,
initial_value: MaybeSignal<ColorMode>, initial_value: MaybeSignal<ColorMode>,
@ -313,6 +290,23 @@ fn get_store_signal(
create_signal(cx, initial_value.get_untracked()) 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 { impl Display for ColorMode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {

View file

@ -1,5 +1,8 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::core::ElementMaybeSignal; use crate::core::ElementMaybeSignal;
use crate::{use_mutation_observer_with_options, watch, watch_with_options, WatchOptions}; use crate::{use_mutation_observer_with_options, watch, watch_with_options, WatchOptions};
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::*; use leptos::*;
use std::marker::PhantomData; 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( pub fn use_css_var(
cx: Scope, cx: Scope,
prop: impl Into<MaybeSignal<String>>, prop: impl Into<MaybeSignal<String>>,
@ -98,6 +104,7 @@ where
let (variable, set_variable) = create_signal(cx, initial_value.clone()); let (variable, set_variable) = create_signal(cx, initial_value.clone());
cfg_if! { if #[cfg(not(feature = "ssr"))] {
let el_signal = (cx, target).into(); let el_signal = (cx, target).into();
let prop = prop.into(); let prop = prop.into();
@ -164,6 +171,7 @@ where
} }
}, },
); );
}}
(variable, set_variable) (variable, set_variable)
} }
@ -192,6 +200,18 @@ where
_marker: PhantomData<T>, _marker: PhantomData<T>,
} }
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> { impl Default for UseCssVarOptions<web_sys::Element, web_sys::Element> {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -202,3 +222,4 @@ impl Default for UseCssVarOptions<web_sys::Element, web_sys::Element> {
} }
} }
} }
}}

View file

@ -31,7 +31,6 @@ use leptos::*;
/// # view! { cx, } /// # view! { cx, }
/// # } /// # }
/// ``` /// ```
pub fn use_cycle_list<T, L>( pub fn use_cycle_list<T, L>(
cx: Scope, cx: Scope,
list: L, list: L,

View file

@ -67,6 +67,11 @@ use std::rc::Rc;
/// ///
/// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle) /// - [**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/) /// - [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>( pub fn use_debounce_fn<F, R>(
func: F, func: F,
ms: impl Into<MaybeSignal<f64>> + 'static, ms: impl Into<MaybeSignal<f64>> + 'static,

View file

@ -1,4 +1,7 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::use_event_listener; use crate::use_event_listener;
use cfg_if::cfg_if;
use leptos::ev::visibilitychange; use leptos::ev::visibilitychange;
use leptos::*; use leptos::*;
@ -21,12 +24,24 @@ use leptos::*;
/// # view! { cx, } /// # 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> { 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 (visibility, set_visibility) = create_signal(cx, inital_visibility);
cfg_if! { if #[cfg(not(feature = "ssr"))] {
let _ = use_event_listener(cx, document(), visibilitychange, move |_| { let _ = use_event_listener(cx, document(), visibilitychange, move |_| {
set_visibility.set(document().visibility_state()); set_visibility.set(document().visibility_state());
}); });
}}
visibility.into() visibility.into()
} }

View file

@ -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> pub fn use_element_hover<El, T>(cx: Scope, el: El) -> Signal<bool>
where where
El: Clone, El: Clone,

View file

@ -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 /// ## See also
/// ///
/// - [`use_resize_observer`] /// - [`use_resize_observer`]

View file

@ -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 /// ## See also
/// ///
/// * [`use_intersection_observer`] /// * [`use_intersection_observer`]

View file

@ -76,11 +76,9 @@ use wasm_bindgen::JsCast;
/// # } /// # }
/// ``` /// ```
/// ///
/// Note if your components also run in SSR (Server Side Rendering), you might get errors /// ## Server-Side Rendering
/// because DOM APIs like document and window are not available outside of the browser. ///
/// To avoid that you can put the logic inside a /// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements)
/// [`create_effect`](https://docs.rs/leptos/latest/leptos/fn.create_effect.html) hook
/// which only runs client side.
pub fn use_event_listener<Ev, El, T, F>( pub fn use_event_listener<Ev, El, T, F>(
cx: Scope, cx: Scope,
target: El, target: El,

View file

@ -1,4 +1,7 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::watch; use crate::watch;
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::*; use leptos::*;
use wasm_bindgen::JsCast; 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>>) { pub fn use_favicon(cx: Scope) -> (ReadSignal<Option<String>>, WriteSignal<Option<String>>) {
use_favicon_with_options(cx, UseFaviconOptions::default()) use_favicon_with_options(cx, UseFaviconOptions::default())
} }
@ -68,14 +74,15 @@ pub fn use_favicon_with_options(
rel, rel,
} = options; } = options;
let link_selector = format!("link[rel*=\"{rel}\"]");
let (favicon, set_favicon) = create_signal(cx, new_icon.get_untracked()); let (favicon, set_favicon) = create_signal(cx, new_icon.get_untracked());
if matches!(&new_icon, MaybeSignal::Dynamic(_)) { if matches!(&new_icon, MaybeSignal::Dynamic(_)) {
create_effect(cx, move |_| set_favicon.set(new_icon.get())); create_effect(cx, move |_| set_favicon.set(new_icon.get()));
} }
cfg_if! { if #[cfg(not(feature = "ssr"))] {
let link_selector = format!("link[rel*=\"{rel}\"]");
let apply_icon = move |icon: &String| { let apply_icon = move |icon: &String| {
if let Some(head) = document().head() { if let Some(head) = document().head() {
if let Ok(links) = head.query_selector_all(&link_selector) { if let Ok(links) = head.query_selector_all(&link_selector) {
@ -101,6 +108,7 @@ pub fn use_favicon_with_options(
} }
}, },
); );
}}
(favicon, set_favicon) (favicon, set_favicon)
} }

View file

@ -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 /// ## See also
/// ///
/// * [`use_element_visibility`] /// * [`use_element_visibility`]

View file

@ -28,6 +28,10 @@ use leptos::*;
/// # view! { cx, } /// # view! { cx, }
/// # } /// # }
/// ``` /// ```
///
/// ## Server-Side Rendering
///
/// On the server this function will simply be ignored.
pub fn use_interval<N>( pub fn use_interval<N>(
cx: Scope, cx: Scope,
interval: N, interval: N,

View file

@ -1,5 +1,8 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::utils::Pausable; use crate::utils::Pausable;
use crate::watch; use crate::watch;
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::leptos_dom::helpers::IntervalHandle; use leptos::leptos_dom::helpers::IntervalHandle;
use leptos::*; use leptos::*;
@ -32,6 +35,10 @@ use std::time::Duration;
/// # view! { cx, } /// # view! { cx, }
/// # } /// # }
/// ``` /// ```
///
/// ## Server-Side Rendering
///
/// On the server this function will simply be ignored.
pub fn use_interval_fn<CbFn, N>( pub fn use_interval_fn<CbFn, N>(
cx: Scope, cx: Scope,
callback: CbFn, callback: CbFn,
@ -86,6 +93,7 @@ where
let interval = interval.into(); let interval = interval.into();
let resume = move || { let resume = move || {
cfg_if! { if #[cfg(not(feature = "ssr"))] {
let interval_value = interval.get(); let interval_value = interval.get();
if interval_value == 0 { if interval_value == 0 {
return; return;
@ -101,6 +109,7 @@ where
timer.set( timer.set(
set_interval_with_handle(callback.clone(), Duration::from_millis(interval_value)).ok(), set_interval_with_handle(callback.clone(), Duration::from_millis(interval_value)).ok(),
); );
}}
}; };
if immediate { if immediate {

View file

@ -1,4 +1,7 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports, dead_code))]
use crate::utils::js_value_from_to_string; use crate::utils::js_value_from_to_string;
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use js_sys::Reflect; use js_sys::Reflect;
use leptos::*; 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 /// 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 /// 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. /// The results can be customized in multiple ways.
/// ///
@ -143,12 +146,20 @@ use wasm_bindgen::{JsCast, JsValue};
/// ///
/// For an exhaustive list of options see [`UseIntlNumberFormatOptions`](https://docs.rs/leptos_use/latest/leptos_use/struct.UseIntlNumberFormatOptions.html). /// 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. /// 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) /// Please see [`UseIntlNumberFormatReturn::format_range`](https://docs.rs/leptos_use/latest/leptos_use/struct.UseIntlNumberFormatReturn.html#method.format_range)
/// for details. /// 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 { pub fn use_intl_number_format(options: UseIntlNumberFormatOptions) -> UseIntlNumberFormatReturn {
cfg_if! { if #[cfg(feature = "ssr")] {
UseIntlNumberFormatReturn
} else {
let number_format = js_sys::Intl::NumberFormat::new( let number_format = js_sys::Intl::NumberFormat::new(
&js_sys::Array::from_iter(options.locales.iter().map(JsValue::from)), &js_sys::Array::from_iter(options.locales.iter().map(JsValue::from)),
&js_sys::Object::from(options), &js_sys::Object::from(options),
@ -157,6 +168,7 @@ pub fn use_intl_number_format(options: UseIntlNumberFormatOptions) -> UseIntlNum
UseIntlNumberFormatReturn { UseIntlNumberFormatReturn {
js_intl_number_format: number_format, js_intl_number_format: number_format,
} }
}}
} }
#[derive(Default, Copy, Clone, PartialEq, Eq, Hash, Debug)] #[derive(Default, Copy, Clone, PartialEq, Eq, Hash, Debug)]
@ -788,21 +800,32 @@ impl From<UseIntlNumberFormatOptions> for js_sys::Object {
} }
} }
cfg_if! { if #[cfg(feature = "ssr")] {
/// Return type of [`use_intl_number_format`].
pub struct UseIntlNumberFormatReturn;
} else {
/// Return type of [`use_intl_number_format`]. /// Return type of [`use_intl_number_format`].
pub struct UseIntlNumberFormatReturn { pub struct UseIntlNumberFormatReturn {
/// The instance of [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat). /// 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, pub js_intl_number_format: js_sys::Intl::NumberFormat,
} }
}}
impl UseIntlNumberFormatReturn { 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. /// 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. /// See [`use_intl_number_format`] for more information.
pub fn format<N>(&self, cx: Scope, number: impl Into<MaybeSignal<N>>) -> Signal<String> pub fn format<N>(&self, cx: Scope, number: impl Into<MaybeSignal<N>>) -> Signal<String>
where where
N: Clone + 'static, N: Clone + Display + 'static,
js_sys::Number: From<N>, js_sys::Number: From<N>,
{ {
let number = number.into(); let number = number.into();
cfg_if! { if #[cfg(feature = "ssr")] {
Signal::derive(cx, move || {
format!("{}", number.get())
})
} else {
let number_format = self.js_intl_number_format.clone(); let number_format = self.js_intl_number_format.clone();
Signal::derive(cx, move || { Signal::derive(cx, move || {
@ -815,6 +838,7 @@ impl UseIntlNumberFormatReturn {
"".to_string() "".to_string()
} }
}) })
}}
} }
/// Formats a range of numbers according to the locale and formatting options of this `Intl.NumberFormat` object. /// Formats a range of numbers according to the locale and formatting options of this `Intl.NumberFormat` object.
@ -870,13 +894,19 @@ impl UseIntlNumberFormatReturn {
end: impl Into<MaybeSignal<NEnd>>, end: impl Into<MaybeSignal<NEnd>>,
) -> Signal<String> ) -> Signal<String>
where where
NStart: Clone + 'static, NStart: Clone + Display + 'static,
NEnd: Clone + 'static, NEnd: Clone + Display + 'static,
js_sys::Number: From<NStart>, js_sys::Number: From<NStart>,
js_sys::Number: From<NEnd>, js_sys::Number: From<NEnd>,
{ {
let start = start.into(); let start = start.into();
let end = end.into(); let end = end.into();
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(); let number_format = self.js_intl_number_format.clone();
Signal::derive(cx, move || { Signal::derive(cx, move || {
@ -894,6 +924,7 @@ impl UseIntlNumberFormatReturn {
"".to_string() "".to_string()
}) })
}}
} }
// TODO : Allows locale-aware formatting of strings produced by this `Intl.NumberFormat` object. // TODO : Allows locale-aware formatting of strings produced by this `Intl.NumberFormat` object.

View file

@ -1,5 +1,8 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports, dead_code))]
use crate::use_event_listener; use crate::use_event_listener;
use crate::utils::CloneableFnMutWithArg; use crate::utils::CloneableFnMutWithArg;
use cfg_if::cfg_if;
use leptos::ev::change; use leptos::ev::change;
use leptos::*; use leptos::*;
use std::cell::RefCell; 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 /// ## See also
/// ///
/// * [`use_preferred_dark`] /// * [`use_preferred_dark`]
@ -37,6 +44,7 @@ pub fn use_media_query(cx: Scope, query: impl Into<MaybeSignal<String>>) -> Sign
let (matches, set_matches) = create_signal(cx, false); let (matches, set_matches) = create_signal(cx, false);
cfg_if! { if #[cfg(not(feature = "ssr"))] {
let media_query: Rc<RefCell<Option<web_sys::MediaQueryList>>> = Rc::new(RefCell::new(None)); let media_query: Rc<RefCell<Option<web_sys::MediaQueryList>>> = Rc::new(RefCell::new(None));
let remove_listener: RemoveListener = Rc::new(RefCell::new(None)); let remove_listener: RemoveListener = Rc::new(RefCell::new(None));
@ -87,6 +95,7 @@ pub fn use_media_query(cx: Scope, query: impl Into<MaybeSignal<String>>) -> Sign
create_effect(cx, move |_| update()); create_effect(cx, move |_| update());
on_cleanup(cx, cleanup); on_cleanup(cx, cleanup);
}}
matches.into() matches.into()
} }

View file

@ -1,5 +1,8 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::core::{ElementMaybeSignal, Position}; use crate::core::{ElementMaybeSignal, Position};
use crate::use_event_listener_with_options; use crate::use_event_listener_with_options;
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::ev::{dragover, mousemove, touchend, touchmove, touchstart}; use leptos::ev::{dragover, mousemove, touchend, touchmove, touchstart};
use leptos::*; use leptos::*;
@ -83,6 +86,10 @@ use web_sys::AddEventListenerOptions;
/// view! { cx, <div node_ref=element></div> } /// 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 { pub fn use_mouse(cx: Scope) -> UseMouseReturn {
use_mouse_with_options(cx, Default::default()) use_mouse_with_options(cx, Default::default())
} }
@ -154,6 +161,7 @@ where
// TODO : event filters? // TODO : event filters?
cfg_if! { if #[cfg(not(feature = "ssr"))] {
let target = options.target; let target = options.target;
let mut event_listener_options = AddEventListenerOptions::new(); let mut event_listener_options = AddEventListenerOptions::new();
event_listener_options.passive(true); event_listener_options.passive(true);
@ -197,6 +205,7 @@ where
); );
} }
} }
}}
UseMouseReturn { UseMouseReturn {
x, x,
@ -235,6 +244,20 @@ where
_marker: PhantomData<T>, _marker: PhantomData<T>,
} }
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> { impl Default for UseMouseOptions<web_sys::Window, web_sys::Window, UseMouseEventExtractorDefault> {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -247,6 +270,7 @@ impl Default for UseMouseOptions<web_sys::Window, web_sys::Window, UseMouseEvent
} }
} }
} }
}}
/// Defines how to get the coordinates from the event. /// Defines how to get the coordinates from the event.
#[derive(Clone)] #[derive(Clone)]

View file

@ -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>( pub fn use_mutation_observer<El, T, F>(
cx: Scope, cx: Scope,
target: El, target: El,

View file

@ -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 /// ## See also
/// ///
/// * [`use_media_query`] /// * [`use_media_query`]

View file

@ -18,6 +18,10 @@ use leptos::*;
/// # } /// # }
/// ``` /// ```
/// ///
/// ## Server-Side Rendering
///
/// On the server this functions returns a Signal that is always `false`.
///
/// ## See also /// ## See also
/// ///
/// * [`use_media_query`] /// * [`use_media_query`]

View file

@ -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 /// ## See also
/// ///
/// - [`use_element_size`] /// - [`use_element_size`]

View file

@ -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 pub fn use_scroll<El, T>(cx: Scope, element: El) -> UseScrollReturn
where where
El: Clone, El: Clone,

View file

@ -63,6 +63,11 @@ pub use crate::utils::ThrottleOptions;
/// ///
/// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle) /// - [**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/) /// - [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>( pub fn use_throttle_fn<F, R>(
func: F, func: F,
ms: impl Into<MaybeSignal<f64>> + 'static, ms: impl Into<MaybeSignal<f64>> + 'static,

View file

@ -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 core::fmt;
use leptos::{leptos_dom::helpers::TimeoutHandle, *};
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
@ -21,7 +23,7 @@ use crate::utils::CloneableFnWithArg;
/// ///
/// ``` /// ```
/// # use leptos::*; /// # use leptos::*;
/// # use leptos_use::websocket::*; /// # use leptos_use::{use_websocket, UseWebSocketReadyState, UseWebsocketReturn};
/// # /// #
/// # #[component] /// # #[component]
/// # fn Demo(cx: Scope) -> impl IntoView { /// # 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( pub fn use_websocket(
cx: Scope, cx: Scope,
url: String, url: String,
@ -87,7 +92,6 @@ pub fn use_websocket(
} }
/// Version of [`use_websocket`] that takes `UseWebSocketOptions`. See [`use_websocket`] for how to use. /// Version of [`use_websocket`] that takes `UseWebSocketOptions`. See [`use_websocket`] for how to use.
// #[doc(cfg(feature = "websocket"))]
pub fn use_websocket_with_options( pub fn use_websocket_with_options(
cx: Scope, cx: Scope,
url: String, url: String,
@ -103,24 +107,26 @@ pub fn use_websocket_with_options(
let (message_bytes, set_message_bytes) = create_signal(cx, None); let (message_bytes, set_message_bytes) = create_signal(cx, None);
let ws_ref: StoredValue<Option<WebSocket>> = store_value(cx, None); let ws_ref: StoredValue<Option<WebSocket>> = store_value(cx, None);
let reconnect_limit = options.reconnect_limit.unwrap_or(3);
let reconnect_timer_ref: StoredValue<Option<TimeoutHandle>> = store_value(cx, None);
let manual = options.manual;
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);
cfg_if! { if #[cfg(not(feature = "ssr"))] {
let on_open_ref = store_value(cx, options.on_open); let on_open_ref = store_value(cx, options.on_open);
let on_message_ref = store_value(cx, options.on_message); let on_message_ref = store_value(cx, options.on_message);
let on_message_bytes_ref = store_value(cx, options.on_message_bytes); let on_message_bytes_ref = store_value(cx, options.on_message_bytes);
let on_error_ref = store_value(cx, options.on_error); let on_error_ref = store_value(cx, options.on_error);
let on_close_ref = store_value(cx, options.on_close); 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_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 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); let reconnect_ref: StoredValue<Option<Rc<dyn Fn()>>> = store_value(cx, None);
reconnect_ref.set_value({ reconnect_ref.set_value({
let ws = ws_ref.get_value(); let ws = ws_ref.get_value();
@ -270,6 +276,7 @@ pub fn use_websocket_with_options(
ws_ref.set_value(Some(web_socket)); ws_ref.set_value(Some(web_socket));
})) }))
}); });
}}
// Send text (String) // Send text (String)
let send = { let send = {
@ -283,29 +290,26 @@ pub fn use_websocket_with_options(
}; };
// Send bytes // Send bytes
let send_bytes = { let send_bytes = move |data: Vec<u8>| {
move |data: Vec<u8>| {
if ready_state.get() == UseWebSocketReadyState::Open { if ready_state.get() == UseWebSocketReadyState::Open {
if let Some(web_socket) = ws_ref.get_value() { if let Some(web_socket) = ws_ref.get_value() {
let _ = web_socket.send_with_u8_array(&data); let _ = web_socket.send_with_u8_array(&data);
} }
} }
}
}; };
// Open connection // Open connection
let open = { let open = move || {
move || {
reconnect_times_ref.set_value(0); reconnect_times_ref.set_value(0);
if let Some(connect) = connect_ref.get_value() { if let Some(connect) = connect_ref.get_value() {
connect(); connect();
} }
}
}; };
// Close connection // Close connection
let close = { let close = {
reconnect_timer_ref.set_value(None); reconnect_timer_ref.set_value(None);
move || { move || {
reconnect_times_ref.set_value(reconnect_limit); reconnect_times_ref.set_value(reconnect_limit);
if let Some(web_socket) = ws_ref.get_value() { if let Some(web_socket) = ws_ref.get_value() {
@ -319,8 +323,6 @@ pub fn use_websocket_with_options(
if !manual { if !manual {
open(); open();
} }
|| ()
}); });
// clean up (unmount) // clean up (unmount)

View file

@ -1,4 +1,7 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::use_event_listener; use crate::use_event_listener;
use cfg_if::cfg_if;
use leptos::ev::{blur, focus}; use leptos::ev::{blur, focus};
use leptos::*; use leptos::*;
@ -22,11 +25,23 @@ use leptos::*;
/// # view! { cx, } /// # 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> { 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 (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(), blur, move |_| set_focused.set(false));
let _ = use_event_listener(cx, window(), focus, move |_| set_focused.set(true)); let _ = use_event_listener(cx, window(), focus, move |_| set_focused.set(true));
}}
focused.into() focused.into()
} }

View file

@ -1,4 +1,7 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::use_event_listener_with_options; use crate::use_event_listener_with_options;
use cfg_if::cfg_if;
use leptos::ev::scroll; use leptos::ev::scroll;
use leptos::*; use leptos::*;
use web_sys::AddEventListenerOptions; use web_sys::AddEventListenerOptions;
@ -22,10 +25,22 @@ use web_sys::AddEventListenerOptions;
/// # view! { cx, } /// # 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>) { pub fn use_window_scroll(cx: Scope) -> (Signal<f64>, Signal<f64>) {
let (x, set_x) = create_signal(cx, window().scroll_x().unwrap_or_default()); cfg_if! { if #[cfg(feature = "ssr")] {
let (y, set_y) = create_signal(cx, window().scroll_y().unwrap_or_default()); 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);
cfg_if! { if #[cfg(not(feature = "ssr"))] {
let mut options = AddEventListenerOptions::new(); let mut options = AddEventListenerOptions::new();
options.capture(false); options.capture(false);
options.passive(true); options.passive(true);
@ -40,6 +55,7 @@ pub fn use_window_scroll(cx: Scope) -> (Signal<f64>, Signal<f64>) {
}, },
options, options,
); );
}}
(x.into(), y.into()) (x.into(), y.into())
} }

View file

@ -1,4 +1,7 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::utils::CloneableFnWithReturn; use crate::utils::CloneableFnWithReturn;
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::leptos_dom::helpers::TimeoutHandle; use leptos::leptos_dom::helpers::TimeoutHandle;
use leptos::{set_timeout_with_handle, MaybeSignal, SignalGetUntracked}; use leptos::{set_timeout_with_handle, MaybeSignal, SignalGetUntracked};
@ -56,6 +59,7 @@ where
return Rc::clone(&last_return_value); return Rc::clone(&last_return_value);
} }
cfg_if! { if #[cfg(not(feature = "ssr"))] {
// Create the max_timer. Clears the regular timer on invoke // Create the max_timer. Clears the regular timer on invoke
if let Some(max_duration) = max_duration { if let Some(max_duration) = max_duration {
if max_timer.get().is_none() { if max_timer.get().is_none() {
@ -87,6 +91,7 @@ where
) )
.ok(), .ok(),
); );
}}
Rc::clone(&last_return_value) Rc::clone(&last_return_value)
} }

View file

@ -1,4 +1,7 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::utils::CloneableFnWithReturn; use crate::utils::CloneableFnWithReturn;
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use js_sys::Date; use js_sys::Date;
use leptos::leptos_dom::helpers::TimeoutHandle; use leptos::leptos_dom::helpers::TimeoutHandle;
@ -72,6 +75,7 @@ where
last_exec.set(Date::now()); last_exec.set(Date::now());
invoke(); invoke();
} else if options.trailing { } else if options.trailing {
cfg_if! { if #[cfg(not(feature = "ssr"))] {
let last_exec = Rc::clone(&last_exec); let last_exec = Rc::clone(&last_exec);
let is_leading = Rc::clone(&is_leading); let is_leading = Rc::clone(&is_leading);
timer.set( timer.set(
@ -86,8 +90,10 @@ where
) )
.ok(), .ok(),
); );
}}
} }
cfg_if! { if #[cfg(not(feature = "ssr"))] {
if !options.leading && timer.get().is_none() { if !options.leading && timer.get().is_none() {
let is_leading = Rc::clone(&is_leading); let is_leading = Rc::clone(&is_leading);
timer.set( timer.set(
@ -100,6 +106,7 @@ where
.ok(), .ok(),
); );
} }
}}
is_leading.set(false); is_leading.set(false);

View file

@ -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 /// ## See also
/// ///
/// * [`watch_throttled`] /// * [`watch_throttled`]

View file

@ -60,6 +60,12 @@ use leptos::*;
/// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle) /// - [**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/) /// - [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 /// ## See also
/// ///
/// * [`watch`] /// * [`watch`]

View file

@ -44,6 +44,12 @@ use leptos::*;
/// ///
/// There's also [`watch_pausable_with_options`] which takes the same options as [`watch`]. /// 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 /// ## See also
/// ///
/// * [`watch`] /// * [`watch`]

View file

@ -60,6 +60,12 @@ use leptos::*;
/// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle) /// - [**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/) /// - [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 /// ## See also
/// ///
/// * [`watch`] /// * [`watch`]

View file

@ -76,6 +76,12 @@ use leptos::*;
/// # view! { cx, } /// # 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 pub fn whenever<T, DFn, CFn>(cx: Scope, source: DFn, callback: CFn) -> impl Fn() + Clone
where where
DFn: Fn() -> bool + 'static, DFn: Fn() -> bool + 'static,