-
- is_over_drop_zone:
-
-
-
- dropped:
-
-
+
is_over_drop_zone:
+
dropped:
-
+
-
- Name:
- {file.name()}
-
-
- Size:
- {file.size()}
-
-
- Type:
- {file.type_()}
-
-
- Last modified:
- {file.last_modified()}
-
+
Name: {file.name()}
+
Size: {file.size()}
+
Type: {file.type_()}
+
Last modified: {file.last_modified()}
diff --git a/examples/use_element_size/.cargo/config.toml b/examples/use_element_size/.cargo/config.toml
deleted file mode 100644
index 8467175..0000000
--- a/examples/use_element_size/.cargo/config.toml
+++ /dev/null
@@ -1,2 +0,0 @@
-[build]
-rustflags = ["--cfg=web_sys_unstable_apis"]
diff --git a/examples/use_event_listener/src/main.rs b/examples/use_event_listener/src/main.rs
index eb2b22d..76c2f62 100644
--- a/examples/use_event_listener/src/main.rs
+++ b/examples/use_event_listener/src/main.rs
@@ -2,11 +2,11 @@ use leptos::ev::{click, keydown};
use leptos::html::A;
use leptos::logging::log;
use leptos::*;
-use leptos_use::use_event_listener;
+use leptos_use::{use_event_listener, use_window};
#[component]
fn Demo() -> impl IntoView {
- let _ = use_event_listener(window(), keydown, |evt| {
+ let _ = use_event_listener(use_window(), keydown, |evt| {
log!("window keydown: '{}'", evt.key());
});
diff --git a/examples/use_geolocation/src/main.rs b/examples/use_geolocation/src/main.rs
index ea2187a..895aa7e 100644
--- a/examples/use_geolocation/src/main.rs
+++ b/examples/use_geolocation/src/main.rs
@@ -27,20 +27,22 @@ fn Demo() -> impl IntoView {
heading: {:?},
speed: {:?},
}}"#,
- coords.accuracy(), coords.latitude(), coords.longitude(), coords.altitude(),
- coords.altitude_accuracy(), coords.heading(), coords.speed()
+ coords.accuracy(),
+ coords.latitude(),
+ coords.longitude(),
+ coords.altitude(),
+ coords.altitude_accuracy(),
+ coords.heading(),
+ coords.speed(),
)
} else {
"None".to_string()
}
}}
,
- located_at:
- {located_at}
- ,
+ located_at: {located_at} ,
error:
- {move || if let Some(error) = error() { error.message() } else { "None".to_string() }}
- ,
+ {move || if let Some(error) = error() { error.message() } else { "None".to_string() }} ,
"Pause watch"
"Resume watch"
diff --git a/examples/use_idle/src/main.rs b/examples/use_idle/src/main.rs
index 3ad03cb..26fb5af 100644
--- a/examples/use_idle/src/main.rs
+++ b/examples/use_idle/src/main.rs
@@ -14,20 +14,11 @@ fn Demo() -> impl IntoView {
view! {
- For demonstration purpose, the idle timeout is set to
-
- 5s
-
+ For demonstration purpose, the idle timeout is set to 5s
in this demo (default 1min).
-
- Idle:
-
-
-
- Inactive:
- {idled_for} s
-
+
Idle:
+
Inactive: {idled_for} s
}
}
diff --git a/examples/use_infinite_scroll/Cargo.toml b/examples/use_infinite_scroll/Cargo.toml
new file mode 100644
index 0000000..93b5074
--- /dev/null
+++ b/examples/use_infinite_scroll/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "use_infinite_scroll"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+leptos = { version = "0.5", features = ["nightly", "csr"] }
+console_error_panic_hook = "0.1"
+console_log = "1"
+log = "0.4"
+leptos-use = { path = "../..", features = ["docs"] }
+web-sys = "0.3"
+
+[dev-dependencies]
+wasm-bindgen = "0.2"
+wasm-bindgen-test = "0.3.0"
diff --git a/examples/use_infinite_scroll/README.md b/examples/use_infinite_scroll/README.md
new file mode 100644
index 0000000..48fe6ea
--- /dev/null
+++ b/examples/use_infinite_scroll/README.md
@@ -0,0 +1,23 @@
+A simple example for `use_infinite_scroll`.
+
+If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation)
+as well as the nightly toolchain for Rust and the wasm32-unknown-unknown target:
+
+```bash
+cargo install trunk
+npm install -D tailwindcss @tailwindcss/forms
+rustup toolchain install nightly
+rustup target add wasm32-unknown-unknown
+```
+
+Then, open two terminals. In the first one, run:
+
+```
+npx tailwindcss -i ./input.css -o ./style/output.css --watch
+```
+
+In the second one, run:
+
+```bash
+trunk serve --open
+```
\ No newline at end of file
diff --git a/examples/use_infinite_scroll/Trunk.toml b/examples/use_infinite_scroll/Trunk.toml
new file mode 100644
index 0000000..3e4be08
--- /dev/null
+++ b/examples/use_infinite_scroll/Trunk.toml
@@ -0,0 +1,2 @@
+[build]
+public_url = "/demo/"
\ No newline at end of file
diff --git a/examples/use_infinite_scroll/index.html b/examples/use_infinite_scroll/index.html
new file mode 100644
index 0000000..ae249a6
--- /dev/null
+++ b/examples/use_infinite_scroll/index.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/examples/use_infinite_scroll/input.css b/examples/use_infinite_scroll/input.css
new file mode 100644
index 0000000..bd6213e
--- /dev/null
+++ b/examples/use_infinite_scroll/input.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
\ No newline at end of file
diff --git a/examples/use_infinite_scroll/rust-toolchain.toml b/examples/use_infinite_scroll/rust-toolchain.toml
new file mode 100644
index 0000000..271800c
--- /dev/null
+++ b/examples/use_infinite_scroll/rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "nightly"
\ No newline at end of file
diff --git a/examples/use_infinite_scroll/src/main.rs b/examples/use_infinite_scroll/src/main.rs
new file mode 100644
index 0000000..57121ca
--- /dev/null
+++ b/examples/use_infinite_scroll/src/main.rs
@@ -0,0 +1,40 @@
+use leptos::html::Div;
+use leptos::*;
+use leptos_use::docs::demo_or_body;
+use leptos_use::{use_infinite_scroll_with_options, UseInfiniteScrollOptions};
+
+#[component]
+fn Demo() -> impl IntoView {
+ let el = create_node_ref::
();
+
+ let (data, set_data) = create_signal(vec![1, 2, 3, 4, 5, 6]);
+
+ let _ = use_infinite_scroll_with_options(
+ el,
+ move |_| async move {
+ let len = data.with_untracked(|d| d.len());
+ set_data.update(|data| *data = (1..len + 6).collect());
+ },
+ UseInfiniteScrollOptions::default().distance(10.0),
+ );
+
+ view! {
+
+ }
+}
+
+fn main() {
+ _ = console_log::init_with_level(log::Level::Debug);
+ console_error_panic_hook::set_once();
+
+ mount_to(demo_or_body(), || {
+ view! {
}
+ })
+}
diff --git a/examples/use_infinite_scroll/style/output.css b/examples/use_infinite_scroll/style/output.css
new file mode 100644
index 0000000..33e91d6
--- /dev/null
+++ b/examples/use_infinite_scroll/style/output.css
@@ -0,0 +1,338 @@
+[type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: #fff;
+ border-color: #6b7280;
+ border-width: 1px;
+ border-radius: 0px;
+ padding-top: 0.5rem;
+ padding-right: 0.75rem;
+ padding-bottom: 0.5rem;
+ padding-left: 0.75rem;
+ font-size: 1rem;
+ line-height: 1.5rem;
+ --tw-shadow: 0 0 #0000;
+}
+
+[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: #2563eb;
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ border-color: #2563eb;
+}
+
+input::-moz-placeholder, textarea::-moz-placeholder {
+ color: #6b7280;
+ opacity: 1;
+}
+
+input::placeholder,textarea::placeholder {
+ color: #6b7280;
+ opacity: 1;
+}
+
+::-webkit-datetime-edit-fields-wrapper {
+ padding: 0;
+}
+
+::-webkit-date-and-time-value {
+ min-height: 1.5em;
+ text-align: inherit;
+}
+
+::-webkit-datetime-edit {
+ display: inline-flex;
+}
+
+::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+select {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
+ background-position: right 0.5rem center;
+ background-repeat: no-repeat;
+ background-size: 1.5em 1.5em;
+ padding-right: 2.5rem;
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+}
+
+[multiple],[size]:where(select:not([size="1"])) {
+ background-image: initial;
+ background-position: initial;
+ background-repeat: unset;
+ background-size: initial;
+ padding-right: 0.75rem;
+ -webkit-print-color-adjust: unset;
+ print-color-adjust: unset;
+}
+
+[type='checkbox'],[type='radio'] {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ padding: 0;
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+ display: inline-block;
+ vertical-align: middle;
+ background-origin: border-box;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ flex-shrink: 0;
+ height: 1rem;
+ width: 1rem;
+ color: #2563eb;
+ background-color: #fff;
+ border-color: #6b7280;
+ border-width: 1px;
+ --tw-shadow: 0 0 #0000;
+}
+
+[type='checkbox'] {
+ border-radius: 0px;
+}
+
+[type='radio'] {
+ border-radius: 100%;
+}
+
+[type='checkbox']:focus,[type='radio']:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
+ --tw-ring-offset-width: 2px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: #2563eb;
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+}
+
+[type='checkbox']:checked,[type='radio']:checked {
+ border-color: transparent;
+ background-color: currentColor;
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+[type='checkbox']:checked {
+ background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
+}
+
+[type='radio']:checked {
+ background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
+}
+
+[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
+ border-color: transparent;
+ background-color: currentColor;
+}
+
+[type='checkbox']:indeterminate {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
+ border-color: transparent;
+ background-color: currentColor;
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
+ border-color: transparent;
+ background-color: currentColor;
+}
+
+[type='file'] {
+ background: unset;
+ border-color: inherit;
+ border-width: 0;
+ border-radius: 0;
+ padding: 0;
+ font-size: unset;
+ line-height: inherit;
+}
+
+[type='file']:focus {
+ outline: 1px solid ButtonText;
+ outline: 1px auto -webkit-focus-ring-color;
+}
+
+*, ::before, ::after {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+}
+
+::backdrop {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+}
+
+.static {
+ position: static;
+}
+
+.m-auto {
+ margin: auto;
+}
+
+.flex {
+ display: flex;
+}
+
+.h-\[300px\] {
+ height: 300px;
+}
+
+.w-\[300px\] {
+ width: 300px;
+}
+
+.flex-col {
+ flex-direction: column;
+}
+
+.gap-2 {
+ gap: 0.5rem;
+}
+
+.overflow-y-scroll {
+ overflow-y: scroll;
+}
+
+.rounded {
+ border-radius: 0.25rem;
+}
+
+.bg-gray-500\/5 {
+ background-color: rgb(107 114 128 / 0.05);
+}
+
+.p-3 {
+ padding: 0.75rem;
+}
+
+.p-4 {
+ padding: 1rem;
+}
+
+.text-\[--brand-color\] {
+ color: var(--brand-color);
+}
+
+.text-green-600 {
+ --tw-text-opacity: 1;
+ color: rgb(22 163 74 / var(--tw-text-opacity));
+}
+
+.opacity-75 {
+ opacity: 0.75;
+}
+
+@media (prefers-color-scheme: dark) {
+ .dark\:text-green-500 {
+ --tw-text-opacity: 1;
+ color: rgb(34 197 94 / var(--tw-text-opacity));
+ }
+}
\ No newline at end of file
diff --git a/examples/use_infinite_scroll/tailwind.config.js b/examples/use_infinite_scroll/tailwind.config.js
new file mode 100644
index 0000000..bc09f5e
--- /dev/null
+++ b/examples/use_infinite_scroll/tailwind.config.js
@@ -0,0 +1,15 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: {
+ files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"],
+ },
+ theme: {
+ extend: {},
+ },
+ corePlugins: {
+ preflight: false,
+ },
+ plugins: [
+ require('@tailwindcss/forms'),
+ ],
+}
\ No newline at end of file
diff --git a/examples/use_mouse/src/main.rs b/examples/use_mouse/src/main.rs
index e96ac32..7269145 100644
--- a/examples/use_mouse/src/main.rs
+++ b/examples/use_mouse/src/main.rs
@@ -42,8 +42,10 @@ fn Demo() -> impl IntoView {
r#" x: {}
y: {}
source_type: {:?}
-"#, mouse_default.x.get(),
- mouse_default.y.get(), mouse_default.source_type.get()
+"#,
+ mouse_default.x.get(),
+ mouse_default.y.get(),
+ mouse_default.source_type.get(),
)
}}
@@ -56,8 +58,10 @@ fn Demo() -> impl IntoView {
r#" x: {}
y: {}
source_type: {:?}
-"#, mouse_with_extractor.x
- .get(), mouse_with_extractor.y.get(), mouse_with_extractor.source_type.get()
+"#,
+ mouse_with_extractor.x.get(),
+ mouse_with_extractor.y.get(),
+ mouse_with_extractor.source_type.get(),
)
}}
diff --git a/examples/use_raf_fn/src/main.rs b/examples/use_raf_fn/src/main.rs
index 4e794a2..5c8b14f 100644
--- a/examples/use_raf_fn/src/main.rs
+++ b/examples/use_raf_fn/src/main.rs
@@ -15,10 +15,7 @@ fn Demo() -> impl IntoView {
});
view! {
-
- Count:
- {count}
-
+
Count: {count}
Pause
diff --git a/examples/use_service_worker/Cargo.toml b/examples/use_service_worker/Cargo.toml
new file mode 100644
index 0000000..878fa9a
--- /dev/null
+++ b/examples/use_service_worker/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "use_service_worker"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+leptos = { version = "0.5", features = ["nightly", "csr"] }
+console_error_panic_hook = "0.1"
+console_log = "1"
+log = "0.4"
+leptos-use = { path = "../..", features = ["docs"] }
+web-sys = "0.3"
+
+[dev-dependencies]
+wasm-bindgen = "0.2"
+wasm-bindgen-test = "0.3.0"
diff --git a/examples/use_service_worker/README.md b/examples/use_service_worker/README.md
new file mode 100644
index 0000000..a4e333d
--- /dev/null
+++ b/examples/use_service_worker/README.md
@@ -0,0 +1,23 @@
+A simple example for `use_service_worker`.
+
+If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation)
+as well as the nightly toolchain for Rust and the wasm32-unknown-unknown target:
+
+```bash
+cargo install trunk
+npm install -D tailwindcss @tailwindcss/forms
+rustup toolchain install nightly
+rustup target add wasm32-unknown-unknown
+```
+
+Then, open two terminals. In the first one, run:
+
+```
+npx tailwindcss -i ./input.css -o ./style/output.css --watch
+```
+
+In the second one, run:
+
+```bash
+trunk serve --open
+```
\ No newline at end of file
diff --git a/examples/use_service_worker/Trunk.toml b/examples/use_service_worker/Trunk.toml
new file mode 100644
index 0000000..341a2b1
--- /dev/null
+++ b/examples/use_service_worker/Trunk.toml
@@ -0,0 +1,7 @@
+[build]
+public_url = "/demo/"
+
+[[hooks]]
+stage = "post_build"
+command = "bash"
+command_arguments = ["post_build.sh"]
diff --git a/examples/use_service_worker/index.html b/examples/use_service_worker/index.html
new file mode 100644
index 0000000..c3733f9
--- /dev/null
+++ b/examples/use_service_worker/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/use_service_worker/input.css b/examples/use_service_worker/input.css
new file mode 100644
index 0000000..bd6213e
--- /dev/null
+++ b/examples/use_service_worker/input.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
\ No newline at end of file
diff --git a/examples/use_service_worker/manifest.json b/examples/use_service_worker/manifest.json
new file mode 100644
index 0000000..8b720cf
--- /dev/null
+++ b/examples/use_service_worker/manifest.json
@@ -0,0 +1,54 @@
+{
+ "name": "Demo",
+ "short_name": "Demo",
+ "icons": [
+ {
+ "src": "./res/icon/maskable_icon_x48.png",
+ "sizes": "48x48",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "./res/icon/maskable_icon_x72.png",
+ "sizes": "72x72",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "./res/icon/maskable_icon_x96.png",
+ "sizes": "96x96",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "./res/icon/maskable_icon_x128.png",
+ "sizes": "128x128",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "./res/icon/maskable_icon_x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "./res/icon/maskable_icon_x384.png",
+ "sizes": "384x384",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "./res/icon/maskable_icon_x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ],
+ "lang": "en-US",
+ "id": "/demo/",
+ "start_url": "/demo/",
+ "display": "standalone",
+ "background_color": "#e52320",
+ "theme_color": "#e52320"
+}
\ No newline at end of file
diff --git a/examples/use_service_worker/post_build.sh b/examples/use_service_worker/post_build.sh
new file mode 100755
index 0000000..e6ebf5a
--- /dev/null
+++ b/examples/use_service_worker/post_build.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+set -e
+
+appName="use_service_worker"
+stylePrefix="output"
+styleFormat="css"
+
+# Extract build version
+indexJsFile=$(find ./dist/.stage -iname "${appName}-*.js")
+echo "Extracting build version from file: ${indexJsFile}"
+regex="(.*)${appName}-(.*).js"
+_src="${indexJsFile}"
+while [[ "${_src}" =~ ${regex} ]]; do
+ buildVersion="${BASH_REMATCH[2]}"
+ _i=${#BASH_REMATCH}
+ _src=${_src:_i}
+done
+if [ -z "${buildVersion}" ]; then
+ echo "Could not determine build version!"
+ exit 1
+fi
+echo "Build-Version is: ${buildVersion}"
+
+# Replace placeholder in service-worker.js
+serviceWorkerJsFile=$(find ./dist/.stage -iname "service-worker.js")
+echo "Replacing {{buildVersion}} placeholder in: ${serviceWorkerJsFile}"
+sed "s/{{buildVersion}}/${buildVersion}/g" "${serviceWorkerJsFile}" > "${serviceWorkerJsFile}.modified"
+mv -f "${serviceWorkerJsFile}.modified" "${serviceWorkerJsFile}"
+
+# Replace placeholder in index.html
+indexHtmlFile=$(find ./dist/.stage -iname "index.html")
+echo "Replacing {{buildVersion}} placeholder in: ${indexHtmlFile}"
+sed "s/{{buildVersion}}/${buildVersion}/g" "${indexHtmlFile}" > "${indexHtmlFile}.modified"
+mv -f "${indexHtmlFile}.modified" "${indexHtmlFile}"
+
+# Extract CSS build version
+indexJsFile=$(find ./dist/.stage -iname "${stylePrefix}-*.${styleFormat}")
+echo "Extracting style build version from file: ${indexJsFile}"
+regex="(.*)${stylePrefix}-(.*).${styleFormat}"
+_src="${indexJsFile}"
+while [[ "${_src}" =~ ${regex} ]]; do
+ cssBuildVersion="${BASH_REMATCH[2]}"
+ _i=${#BASH_REMATCH}
+ _src=${_src:_i}
+done
+if [ -z "${cssBuildVersion}" ]; then
+ echo "Could not determine style build version!"
+ exit 1
+fi
+echo "CSS Build-Version is: ${cssBuildVersion}"
+
+# Replace placeholder in service-worker.js
+serviceWorkerJsFile=$(find ./dist/.stage -iname "service-worker.js")
+echo "Replacing {{cssBuildVersion}} placeholder in: ${serviceWorkerJsFile}"
+sed "s/{{cssBuildVersion}}/${cssBuildVersion}/g" "${serviceWorkerJsFile}" > "${serviceWorkerJsFile}.modified"
+mv -f "${serviceWorkerJsFile}.modified" "${serviceWorkerJsFile}"
diff --git a/examples/use_service_worker/res/icon/maskable_icon_x128.png b/examples/use_service_worker/res/icon/maskable_icon_x128.png
new file mode 100644
index 0000000..f730574
Binary files /dev/null and b/examples/use_service_worker/res/icon/maskable_icon_x128.png differ
diff --git a/examples/use_service_worker/res/icon/maskable_icon_x192.png b/examples/use_service_worker/res/icon/maskable_icon_x192.png
new file mode 100644
index 0000000..6e308ac
Binary files /dev/null and b/examples/use_service_worker/res/icon/maskable_icon_x192.png differ
diff --git a/examples/use_service_worker/res/icon/maskable_icon_x384.png b/examples/use_service_worker/res/icon/maskable_icon_x384.png
new file mode 100644
index 0000000..3041f52
Binary files /dev/null and b/examples/use_service_worker/res/icon/maskable_icon_x384.png differ
diff --git a/examples/use_service_worker/res/icon/maskable_icon_x48.png b/examples/use_service_worker/res/icon/maskable_icon_x48.png
new file mode 100644
index 0000000..50e8754
Binary files /dev/null and b/examples/use_service_worker/res/icon/maskable_icon_x48.png differ
diff --git a/examples/use_service_worker/res/icon/maskable_icon_x512.png b/examples/use_service_worker/res/icon/maskable_icon_x512.png
new file mode 100644
index 0000000..d1e37b9
Binary files /dev/null and b/examples/use_service_worker/res/icon/maskable_icon_x512.png differ
diff --git a/examples/use_service_worker/res/icon/maskable_icon_x72.png b/examples/use_service_worker/res/icon/maskable_icon_x72.png
new file mode 100644
index 0000000..4196897
Binary files /dev/null and b/examples/use_service_worker/res/icon/maskable_icon_x72.png differ
diff --git a/examples/use_service_worker/res/icon/maskable_icon_x96.png b/examples/use_service_worker/res/icon/maskable_icon_x96.png
new file mode 100644
index 0000000..32b9490
Binary files /dev/null and b/examples/use_service_worker/res/icon/maskable_icon_x96.png differ
diff --git a/examples/use_service_worker/res/icon/pwa.png b/examples/use_service_worker/res/icon/pwa.png
new file mode 100644
index 0000000..b5a31af
Binary files /dev/null and b/examples/use_service_worker/res/icon/pwa.png differ
diff --git a/examples/use_service_worker/res/icon/pwa.svg b/examples/use_service_worker/res/icon/pwa.svg
new file mode 100644
index 0000000..55264a9
--- /dev/null
+++ b/examples/use_service_worker/res/icon/pwa.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/use_service_worker/rust-toolchain.toml b/examples/use_service_worker/rust-toolchain.toml
new file mode 100644
index 0000000..271800c
--- /dev/null
+++ b/examples/use_service_worker/rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "nightly"
\ No newline at end of file
diff --git a/examples/use_service_worker/service-worker.js b/examples/use_service_worker/service-worker.js
new file mode 100644
index 0000000..511a41b
--- /dev/null
+++ b/examples/use_service_worker/service-worker.js
@@ -0,0 +1,82 @@
+var buildVersion = "{{buildVersion}}"
+var cssBuildVersion = "{{cssBuildVersion}}"
+var cacheName = "demo";
+var filesToCache = [
+ './',
+ './index.html',
+ './manifest.json',
+ './use_service_worker-' + buildVersion + '_bg.wasm',
+ './use_service_worker-' + buildVersion + '.js',
+ './output-' + cssBuildVersion + '.css',
+ './res/icon/maskable_icon_x48.png',
+ './res/icon/maskable_icon_x72.png',
+ './res/icon/maskable_icon_x96.png',
+ './res/icon/maskable_icon_x128.png',
+ './res/icon/maskable_icon_x192.png',
+ './res/icon/maskable_icon_x384.png',
+ './res/icon/maskable_icon_x512.png',
+
+ // TODO: Add files you want the SW to cache. Rename entries to match your build output!
+];
+
+/* Start the service worker and cache all of the app's content */
+self.addEventListener('install', function (event) {
+ console.log("Installing service-worker for build", buildVersion);
+ const preCache = async () => {
+ get_cache().then(function (cache) {
+ // We clear the whole cache, as we do not know which resources were updated!
+ cache.keys().then(function (requests) {
+ for (let request of requests) {
+ cache.delete(request);
+ }
+ });
+ cache.addAll(filesToCache.map(url => new Request(url, { credentials: 'same-origin' })));
+ })
+ };
+ event.waitUntil(preCache);
+});
+
+self.addEventListener('message', function (messageEvent) {
+ if (messageEvent.data === "skipWaiting") {
+ console.log("Service-worker received skipWaiting event", buildVersion);
+ self.skipWaiting();
+ }
+});
+
+self.addEventListener('fetch', function (e) {
+ e.respondWith(cache_then_network(e.request));
+});
+
+async function get_cache() {
+ return caches.open(cacheName);
+}
+
+async function cache_then_network(request) {
+ const cache = await get_cache();
+ return cache.match(request).then(
+ (cache_response) => {
+ if (!cache_response) {
+ return fetch_from_network(request, cache);
+ } else {
+ return cache_response;
+ }
+ },
+ (reason) => {
+ return fetch_from_network(request, cache);
+ }
+ );
+}
+
+function fetch_from_network(request, cache) {
+ return fetch(request).then(
+ (net_response) => {
+ return net_response;
+ },
+ (reason) => {
+ console.error("Network fetch rejected. Falling back to ./index.html. Reason: ", reason);
+ return cache.match("./index.html").then(function (cache_root_response) {
+ return cache_root_response;
+ });
+ }
+ )
+}
diff --git a/examples/use_service_worker/src/main.rs b/examples/use_service_worker/src/main.rs
new file mode 100644
index 0000000..93dc1fa
--- /dev/null
+++ b/examples/use_service_worker/src/main.rs
@@ -0,0 +1,59 @@
+use leptos::*;
+use leptos_use::docs::{demo_or_body, BooleanDisplay};
+use leptos_use::{use_document, use_service_worker, UseServiceWorkerReturn};
+use web_sys::HtmlMetaElement;
+
+#[component]
+fn Demo() -> impl IntoView {
+ let build = load_meta_element("version")
+ .map(|meta| meta.content())
+ .expect("'version' meta element");
+
+ let UseServiceWorkerReturn {
+ registration,
+ installing,
+ waiting,
+ active,
+ skip_waiting,
+ ..
+ } = use_service_worker();
+
+ view! {
+
"Current build: " {build}
+
+
+
+
"registration: " {move || format!("{:#?}", registration())}
+
"installing: "
+
"waiting: "
+
"active: "
+
+
+
+
"Send skip_waiting event"
+ }
+}
+
+fn main() {
+ _ = console_log::init_with_level(log::Level::Debug);
+ console_error_panic_hook::set_once();
+
+ mount_to(demo_or_body(), || {
+ view! {
}
+ })
+}
+
+fn load_meta_element(name: &str) -> Result
{
+ use wasm_bindgen::JsCast;
+ if let Some(document) = &*use_document() {
+ document
+ .query_selector(format!("meta[name=\"{name}\"]").as_str())
+ .ok()
+ .flatten()
+ .ok_or_else(|| format!("Unable to find meta element with name '{name}'."))?
+ .dyn_into::()
+ .map_err(|err| format!("Unable to cast element to HtmlMetaElement. Err: '{err:?}'."))
+ } else {
+ Err("Unable to find document.".into())
+ }
+}
diff --git a/examples/use_service_worker/style/output.css b/examples/use_service_worker/style/output.css
new file mode 100644
index 0000000..1c83da6
--- /dev/null
+++ b/examples/use_service_worker/style/output.css
@@ -0,0 +1,294 @@
+[type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: #fff;
+ border-color: #6b7280;
+ border-width: 1px;
+ border-radius: 0px;
+ padding-top: 0.5rem;
+ padding-right: 0.75rem;
+ padding-bottom: 0.5rem;
+ padding-left: 0.75rem;
+ font-size: 1rem;
+ line-height: 1.5rem;
+ --tw-shadow: 0 0 #0000;
+}
+
+[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: #2563eb;
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ border-color: #2563eb;
+}
+
+input::-moz-placeholder, textarea::-moz-placeholder {
+ color: #6b7280;
+ opacity: 1;
+}
+
+input::placeholder,textarea::placeholder {
+ color: #6b7280;
+ opacity: 1;
+}
+
+::-webkit-datetime-edit-fields-wrapper {
+ padding: 0;
+}
+
+::-webkit-date-and-time-value {
+ min-height: 1.5em;
+ text-align: inherit;
+}
+
+::-webkit-datetime-edit {
+ display: inline-flex;
+}
+
+::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+select {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
+ background-position: right 0.5rem center;
+ background-repeat: no-repeat;
+ background-size: 1.5em 1.5em;
+ padding-right: 2.5rem;
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+}
+
+[multiple],[size]:where(select:not([size="1"])) {
+ background-image: initial;
+ background-position: initial;
+ background-repeat: unset;
+ background-size: initial;
+ padding-right: 0.75rem;
+ -webkit-print-color-adjust: unset;
+ print-color-adjust: unset;
+}
+
+[type='checkbox'],[type='radio'] {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ padding: 0;
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+ display: inline-block;
+ vertical-align: middle;
+ background-origin: border-box;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ flex-shrink: 0;
+ height: 1rem;
+ width: 1rem;
+ color: #2563eb;
+ background-color: #fff;
+ border-color: #6b7280;
+ border-width: 1px;
+ --tw-shadow: 0 0 #0000;
+}
+
+[type='checkbox'] {
+ border-radius: 0px;
+}
+
+[type='radio'] {
+ border-radius: 100%;
+}
+
+[type='checkbox']:focus,[type='radio']:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
+ --tw-ring-offset-width: 2px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: #2563eb;
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+}
+
+[type='checkbox']:checked,[type='radio']:checked {
+ border-color: transparent;
+ background-color: currentColor;
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+[type='checkbox']:checked {
+ background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
+}
+
+[type='radio']:checked {
+ background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
+}
+
+[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
+ border-color: transparent;
+ background-color: currentColor;
+}
+
+[type='checkbox']:indeterminate {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
+ border-color: transparent;
+ background-color: currentColor;
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
+ border-color: transparent;
+ background-color: currentColor;
+}
+
+[type='file'] {
+ background: unset;
+ border-color: inherit;
+ border-width: 0;
+ border-radius: 0;
+ padding: 0;
+ font-size: unset;
+ line-height: inherit;
+}
+
+[type='file']:focus {
+ outline: 1px solid ButtonText;
+ outline: 1px auto -webkit-focus-ring-color;
+}
+
+*, ::before, ::after {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+}
+
+::backdrop {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+}
+
+.static {
+ position: static;
+}
+
+.text-\[--brand-color\] {
+ color: var(--brand-color);
+}
+
+.text-green-600 {
+ --tw-text-opacity: 1;
+ color: rgb(22 163 74 / var(--tw-text-opacity));
+}
+
+.opacity-75 {
+ opacity: 0.75;
+}
+
+@media (prefers-color-scheme: dark) {
+ .dark\:text-green-500 {
+ --tw-text-opacity: 1;
+ color: rgb(34 197 94 / var(--tw-text-opacity));
+ }
+}
\ No newline at end of file
diff --git a/examples/use_service_worker/tailwind.config.js b/examples/use_service_worker/tailwind.config.js
new file mode 100644
index 0000000..bc09f5e
--- /dev/null
+++ b/examples/use_service_worker/tailwind.config.js
@@ -0,0 +1,15 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: {
+ files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"],
+ },
+ theme: {
+ extend: {},
+ },
+ corePlugins: {
+ preflight: false,
+ },
+ plugins: [
+ require('@tailwindcss/forms'),
+ ],
+}
\ No newline at end of file
diff --git a/examples/use_sorted/Cargo.toml b/examples/use_sorted/Cargo.toml
new file mode 100644
index 0000000..6b5a699
--- /dev/null
+++ b/examples/use_sorted/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "use_sorted"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+leptos = { version = "0.5", features = ["nightly", "csr"] }
+console_error_panic_hook = "0.1"
+console_log = "1"
+log = "0.4"
+leptos-use = { path = "../..", features = ["docs"] }
+web-sys = "0.3"
+
+[dev-dependencies]
+wasm-bindgen = "0.2"
+wasm-bindgen-test = "0.3.0"
diff --git a/examples/use_sorted/README.md b/examples/use_sorted/README.md
new file mode 100644
index 0000000..25443d9
--- /dev/null
+++ b/examples/use_sorted/README.md
@@ -0,0 +1,23 @@
+A simple example for `use_sorted`.
+
+If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation)
+as well as the nightly toolchain for Rust and the wasm32-unknown-unknown target:
+
+```bash
+cargo install trunk
+npm install -D tailwindcss @tailwindcss/forms
+rustup toolchain install nightly
+rustup target add wasm32-unknown-unknown
+```
+
+Then, open two terminals. In the first one, run:
+
+```
+npx tailwindcss -i ./input.css -o ./style/output.css --watch
+```
+
+In the second one, run:
+
+```bash
+trunk serve --open
+```
\ No newline at end of file
diff --git a/examples/use_sorted/Trunk.toml b/examples/use_sorted/Trunk.toml
new file mode 100644
index 0000000..3e4be08
--- /dev/null
+++ b/examples/use_sorted/Trunk.toml
@@ -0,0 +1,2 @@
+[build]
+public_url = "/demo/"
\ No newline at end of file
diff --git a/examples/use_sorted/index.html b/examples/use_sorted/index.html
new file mode 100644
index 0000000..ae249a6
--- /dev/null
+++ b/examples/use_sorted/index.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/examples/use_sorted/input.css b/examples/use_sorted/input.css
new file mode 100644
index 0000000..bd6213e
--- /dev/null
+++ b/examples/use_sorted/input.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
\ No newline at end of file
diff --git a/examples/use_sorted/rust-toolchain.toml b/examples/use_sorted/rust-toolchain.toml
new file mode 100644
index 0000000..271800c
--- /dev/null
+++ b/examples/use_sorted/rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "nightly"
\ No newline at end of file
diff --git a/examples/use_sorted/src/main.rs b/examples/use_sorted/src/main.rs
new file mode 100644
index 0000000..4ffb11d
--- /dev/null
+++ b/examples/use_sorted/src/main.rs
@@ -0,0 +1,44 @@
+use leptos::*;
+use leptos_use::docs::demo_or_body;
+use leptos_use::use_sorted;
+
+fn string_list(list: &[i32]) -> String {
+ list.into_iter()
+ .map(i32::to_string)
+ .collect::>()
+ .join(",")
+}
+
+#[component]
+fn Demo() -> impl IntoView {
+ let (list, set_list) = create_signal::>(vec![4, 2, 67, 34, 76, 22, 2, 4, 65, 23]);
+
+ let sorted: Signal> = use_sorted(list);
+
+ let on_input = move |evt| {
+ set_list.update(|list| {
+ *list = event_target_value(&evt)
+ .split(",")
+ .map(|n| n.parse::().unwrap_or(0))
+ .collect::>()
+ });
+ };
+
+ let input_text = move || string_list(&list());
+ let sorted_text = move || string_list(&sorted());
+
+ view! {
+ Input:
+
+ Output: {sorted_text}
+ }
+}
+
+fn main() {
+ _ = console_log::init_with_level(log::Level::Debug);
+ console_error_panic_hook::set_once();
+
+ mount_to(demo_or_body(), || {
+ view! { }
+ })
+}
diff --git a/examples/use_sorted/style/output.css b/examples/use_sorted/style/output.css
new file mode 100644
index 0000000..ab5191f
--- /dev/null
+++ b/examples/use_sorted/style/output.css
@@ -0,0 +1,289 @@
+[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: #fff;
+ border-color: #6b7280;
+ border-width: 1px;
+ border-radius: 0px;
+ padding-top: 0.5rem;
+ padding-right: 0.75rem;
+ padding-bottom: 0.5rem;
+ padding-left: 0.75rem;
+ font-size: 1rem;
+ line-height: 1.5rem;
+ --tw-shadow: 0 0 #0000;
+}
+
+[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: #2563eb;
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ border-color: #2563eb;
+}
+
+input::-moz-placeholder, textarea::-moz-placeholder {
+ color: #6b7280;
+ opacity: 1;
+}
+
+input::placeholder,textarea::placeholder {
+ color: #6b7280;
+ opacity: 1;
+}
+
+::-webkit-datetime-edit-fields-wrapper {
+ padding: 0;
+}
+
+::-webkit-date-and-time-value {
+ min-height: 1.5em;
+}
+
+::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+select {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
+ background-position: right 0.5rem center;
+ background-repeat: no-repeat;
+ background-size: 1.5em 1.5em;
+ padding-right: 2.5rem;
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+}
+
+[multiple] {
+ background-image: initial;
+ background-position: initial;
+ background-repeat: unset;
+ background-size: initial;
+ padding-right: 0.75rem;
+ -webkit-print-color-adjust: unset;
+ print-color-adjust: unset;
+}
+
+[type='checkbox'],[type='radio'] {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ padding: 0;
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+ display: inline-block;
+ vertical-align: middle;
+ background-origin: border-box;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ flex-shrink: 0;
+ height: 1rem;
+ width: 1rem;
+ color: #2563eb;
+ background-color: #fff;
+ border-color: #6b7280;
+ border-width: 1px;
+ --tw-shadow: 0 0 #0000;
+}
+
+[type='checkbox'] {
+ border-radius: 0px;
+}
+
+[type='radio'] {
+ border-radius: 100%;
+}
+
+[type='checkbox']:focus,[type='radio']:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
+ --tw-ring-offset-width: 2px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: #2563eb;
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+}
+
+[type='checkbox']:checked,[type='radio']:checked {
+ border-color: transparent;
+ background-color: currentColor;
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+[type='checkbox']:checked {
+ background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
+}
+
+[type='radio']:checked {
+ background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
+}
+
+[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
+ border-color: transparent;
+ background-color: currentColor;
+}
+
+[type='checkbox']:indeterminate {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
+ border-color: transparent;
+ background-color: currentColor;
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
+ border-color: transparent;
+ background-color: currentColor;
+}
+
+[type='file'] {
+ background: unset;
+ border-color: inherit;
+ border-width: 0;
+ border-radius: 0;
+ padding: 0;
+ font-size: unset;
+ line-height: inherit;
+}
+
+[type='file']:focus {
+ outline: 1px solid ButtonText;
+ outline: 1px auto -webkit-focus-ring-color;
+}
+
+*, ::before, ::after {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+}
+
+::backdrop {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+}
+
+.block {
+ display: block;
+}
+
+.text-\[--brand-color\] {
+ color: var(--brand-color);
+}
+
+.text-green-600 {
+ --tw-text-opacity: 1;
+ color: rgb(22 163 74 / var(--tw-text-opacity));
+}
+
+.opacity-75 {
+ opacity: 0.75;
+}
+
+@media (prefers-color-scheme: dark) {
+ .dark\:text-green-500 {
+ --tw-text-opacity: 1;
+ color: rgb(34 197 94 / var(--tw-text-opacity));
+ }
+}
\ No newline at end of file
diff --git a/examples/use_sorted/tailwind.config.js b/examples/use_sorted/tailwind.config.js
new file mode 100644
index 0000000..bc09f5e
--- /dev/null
+++ b/examples/use_sorted/tailwind.config.js
@@ -0,0 +1,15 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: {
+ files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"],
+ },
+ theme: {
+ extend: {},
+ },
+ corePlugins: {
+ preflight: false,
+ },
+ plugins: [
+ require('@tailwindcss/forms'),
+ ],
+}
\ No newline at end of file
diff --git a/examples/use_storage/Cargo.toml b/examples/use_storage/Cargo.toml
index 09446d5..6686fe8 100644
--- a/examples/use_storage/Cargo.toml
+++ b/examples/use_storage/Cargo.toml
@@ -8,7 +8,7 @@ leptos = { version = "0.5", features = ["nightly", "csr"] }
console_error_panic_hook = "0.1"
console_log = "1"
log = "0.4"
-leptos-use = { path = "../..", features = ["docs", "storage"] }
+leptos-use = { path = "../..", features = ["docs", "prost", "serde"] }
web-sys = "0.3"
serde = "1.0.163"
diff --git a/examples/use_storage/src/main.rs b/examples/use_storage/src/main.rs
index 7f5442f..c1a98fa 100644
--- a/examples/use_storage/src/main.rs
+++ b/examples/use_storage/src/main.rs
@@ -1,28 +1,31 @@
use leptos::*;
use leptos_use::docs::{demo_or_body, Note};
-use leptos_use::storage::use_storage;
+use leptos_use::storage::{use_local_storage, JsonCodec};
use serde::{Deserialize, Serialize};
-#[derive(Serialize, Deserialize, Clone, Debug)]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct BananaState {
pub name: String,
- pub color: String,
- pub size: String,
+ pub wearing: String,
+ pub descending: String,
pub count: u32,
}
+impl Default for BananaState {
+ fn default() -> Self {
+ Self {
+ name: "Bananas".to_string(),
+ wearing: "pyjamas".to_string(),
+ descending: "stairs".to_string(),
+ count: 2,
+ }
+ }
+}
+
#[component]
fn Demo() -> impl IntoView {
- let the_default = BananaState {
- name: "Banana".to_string(),
- color: "Yellow".to_string(),
- size: "Medium".to_string(),
- count: 0,
- };
-
- let (state, set_state, _) = use_storage("banana-state", the_default.clone());
-
- let (state2, ..) = use_storage("banana-state", the_default.clone());
+ let (state, set_state, reset) = use_local_storage::("banana-state");
+ let (state2, _, _) = use_local_storage::("banana-state");
view! {
impl IntoView {
/>
impl IntoView {
step="1"
max="1000"
/>
+ "Delete from storage"
"Second "
@@ -67,7 +71,9 @@ fn Demo() -> impl IntoView {
{move || format!("{:#?}", state2.get())}
- "The values are persistent. When you reload the page the values will be the same."
+ "The values are persistent. When you reload the page or "
+ "open a second window"
+ ", the values will be the same."
}
}
diff --git a/examples/use_timestamp/src/main.rs b/examples/use_timestamp/src/main.rs
index b1e79ad..f67a8eb 100644
--- a/examples/use_timestamp/src/main.rs
+++ b/examples/use_timestamp/src/main.rs
@@ -6,12 +6,7 @@ use leptos_use::use_timestamp;
fn Demo() -> impl IntoView {
let timestamp = use_timestamp();
- view! {
-
- Timestamp:
- {timestamp}
-
- }
+ view! { Timestamp: {timestamp}
}
}
fn main() {
diff --git a/examples/use_web_notification/Cargo.toml b/examples/use_web_notification/Cargo.toml
new file mode 100644
index 0000000..26ee6c9
--- /dev/null
+++ b/examples/use_web_notification/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "use_web_notification"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+leptos = { version = "0.5", features = ["nightly", "csr"] }
+console_error_panic_hook = "0.1"
+console_log = "1"
+log = "0.4"
+leptos-use = { path = "../..", features = ["docs"] }
+web-sys = "0.3"
+
+[dev-dependencies]
+wasm-bindgen = "0.2"
+wasm-bindgen-test = "0.3.0"
diff --git a/examples/use_web_notification/README.md b/examples/use_web_notification/README.md
new file mode 100644
index 0000000..aa9d332
--- /dev/null
+++ b/examples/use_web_notification/README.md
@@ -0,0 +1,23 @@
+A simple example for `use_web_notification`.
+
+If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation)
+as well as the nightly toolchain for Rust and the wasm32-unknown-unknown target:
+
+```bash
+cargo install trunk
+npm install -D tailwindcss @tailwindcss/forms
+rustup toolchain install nightly
+rustup target add wasm32-unknown-unknown
+```
+
+Then, open two terminals. In the first one, run:
+
+```
+npx tailwindcss -i ./input.css -o ./style/output.css --watch
+```
+
+In the second one, run:
+
+```bash
+trunk serve --open
+```
\ No newline at end of file
diff --git a/examples/use_web_notification/Trunk.toml b/examples/use_web_notification/Trunk.toml
new file mode 100644
index 0000000..3e4be08
--- /dev/null
+++ b/examples/use_web_notification/Trunk.toml
@@ -0,0 +1,2 @@
+[build]
+public_url = "/demo/"
\ No newline at end of file
diff --git a/examples/use_web_notification/index.html b/examples/use_web_notification/index.html
new file mode 100644
index 0000000..ae249a6
--- /dev/null
+++ b/examples/use_web_notification/index.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/examples/use_web_notification/input.css b/examples/use_web_notification/input.css
new file mode 100644
index 0000000..bd6213e
--- /dev/null
+++ b/examples/use_web_notification/input.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
\ No newline at end of file
diff --git a/examples/use_web_notification/rust-toolchain.toml b/examples/use_web_notification/rust-toolchain.toml
new file mode 100644
index 0000000..271800c
--- /dev/null
+++ b/examples/use_web_notification/rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "nightly"
\ No newline at end of file
diff --git a/examples/use_web_notification/src/main.rs b/examples/use_web_notification/src/main.rs
new file mode 100644
index 0000000..bc717bb
--- /dev/null
+++ b/examples/use_web_notification/src/main.rs
@@ -0,0 +1,52 @@
+use leptos::*;
+use leptos_use::docs::{demo_or_body, BooleanDisplay};
+use leptos_use::{
+ use_web_notification_with_options, NotificationDirection, ShowOptions,
+ UseWebNotificationOptions, UseWebNotificationReturn,
+};
+
+#[component]
+fn Demo() -> impl IntoView {
+ let UseWebNotificationReturn {
+ is_supported, show, ..
+ } = use_web_notification_with_options(
+ UseWebNotificationOptions::default()
+ .title("Hello World from leptos-use")
+ .direction(NotificationDirection::Auto)
+ .language("en")
+ // .renotify(true)
+ .tag("test"),
+ );
+
+ let show = move || {
+ show(ShowOptions::default());
+ };
+
+ view! {
+
+
+ The Notification Web API is not supported in your browser. }
+ }
+ >
+
+
Show Notification
+
+ }
+}
+
+fn main() {
+ _ = console_log::init_with_level(log::Level::Debug);
+ console_error_panic_hook::set_once();
+
+ mount_to(demo_or_body(), || {
+ view! {
}
+ })
+}
diff --git a/examples/use_web_notification/style/output.css b/examples/use_web_notification/style/output.css
new file mode 100644
index 0000000..ab5191f
--- /dev/null
+++ b/examples/use_web_notification/style/output.css
@@ -0,0 +1,289 @@
+[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: #fff;
+ border-color: #6b7280;
+ border-width: 1px;
+ border-radius: 0px;
+ padding-top: 0.5rem;
+ padding-right: 0.75rem;
+ padding-bottom: 0.5rem;
+ padding-left: 0.75rem;
+ font-size: 1rem;
+ line-height: 1.5rem;
+ --tw-shadow: 0 0 #0000;
+}
+
+[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: #2563eb;
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ border-color: #2563eb;
+}
+
+input::-moz-placeholder, textarea::-moz-placeholder {
+ color: #6b7280;
+ opacity: 1;
+}
+
+input::placeholder,textarea::placeholder {
+ color: #6b7280;
+ opacity: 1;
+}
+
+::-webkit-datetime-edit-fields-wrapper {
+ padding: 0;
+}
+
+::-webkit-date-and-time-value {
+ min-height: 1.5em;
+}
+
+::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+select {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
+ background-position: right 0.5rem center;
+ background-repeat: no-repeat;
+ background-size: 1.5em 1.5em;
+ padding-right: 2.5rem;
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+}
+
+[multiple] {
+ background-image: initial;
+ background-position: initial;
+ background-repeat: unset;
+ background-size: initial;
+ padding-right: 0.75rem;
+ -webkit-print-color-adjust: unset;
+ print-color-adjust: unset;
+}
+
+[type='checkbox'],[type='radio'] {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ padding: 0;
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+ display: inline-block;
+ vertical-align: middle;
+ background-origin: border-box;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ flex-shrink: 0;
+ height: 1rem;
+ width: 1rem;
+ color: #2563eb;
+ background-color: #fff;
+ border-color: #6b7280;
+ border-width: 1px;
+ --tw-shadow: 0 0 #0000;
+}
+
+[type='checkbox'] {
+ border-radius: 0px;
+}
+
+[type='radio'] {
+ border-radius: 100%;
+}
+
+[type='checkbox']:focus,[type='radio']:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
+ --tw-ring-offset-width: 2px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: #2563eb;
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+}
+
+[type='checkbox']:checked,[type='radio']:checked {
+ border-color: transparent;
+ background-color: currentColor;
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+[type='checkbox']:checked {
+ background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
+}
+
+[type='radio']:checked {
+ background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
+}
+
+[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
+ border-color: transparent;
+ background-color: currentColor;
+}
+
+[type='checkbox']:indeterminate {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
+ border-color: transparent;
+ background-color: currentColor;
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
+ border-color: transparent;
+ background-color: currentColor;
+}
+
+[type='file'] {
+ background: unset;
+ border-color: inherit;
+ border-width: 0;
+ border-radius: 0;
+ padding: 0;
+ font-size: unset;
+ line-height: inherit;
+}
+
+[type='file']:focus {
+ outline: 1px solid ButtonText;
+ outline: 1px auto -webkit-focus-ring-color;
+}
+
+*, ::before, ::after {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+}
+
+::backdrop {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+}
+
+.block {
+ display: block;
+}
+
+.text-\[--brand-color\] {
+ color: var(--brand-color);
+}
+
+.text-green-600 {
+ --tw-text-opacity: 1;
+ color: rgb(22 163 74 / var(--tw-text-opacity));
+}
+
+.opacity-75 {
+ opacity: 0.75;
+}
+
+@media (prefers-color-scheme: dark) {
+ .dark\:text-green-500 {
+ --tw-text-opacity: 1;
+ color: rgb(34 197 94 / var(--tw-text-opacity));
+ }
+}
\ No newline at end of file
diff --git a/examples/use_web_notification/tailwind.config.js b/examples/use_web_notification/tailwind.config.js
new file mode 100644
index 0000000..bc09f5e
--- /dev/null
+++ b/examples/use_web_notification/tailwind.config.js
@@ -0,0 +1,15 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: {
+ files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"],
+ },
+ theme: {
+ extend: {},
+ },
+ corePlugins: {
+ preflight: false,
+ },
+ plugins: [
+ require('@tailwindcss/forms'),
+ ],
+}
\ No newline at end of file
diff --git a/examples/use_webtransport_with_server/server/cert.crt b/examples/use_webtransport_with_server/server/cert.crt
new file mode 100644
index 0000000..3d4b2c1
--- /dev/null
+++ b/examples/use_webtransport_with_server/server/cert.crt
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEEDCCAvigAwIBAgIUM7q41K5+wQF+tIUZVVjHRLVP8WYwDQYJKoZIhvcNAQEL
+BQAwgZAxCzAJBgNVBAYTAkVTMREwDwYDVQQIDAhUZW5lcmlmZTETMBEGA1UEBwwK
+R3JhbmFkaWxsYTETMBEGA1UECgwKU3lucGhvbnl0ZTEcMBoGA1UEAwwTUm9vdCBD
+ZXJ0IEF1dGhvcml0eTEmMCQGCSqGSIb3DQEJARYXbWFjY2VzY2hAc3lucGhvbnl0
+ZS5jb20wHhcNMjMwOTE3MjI0NDExWhcNMjUxMjIwMjI0NDExWjCBgDELMAkGA1UE
+BhMCRVMxETAPBgNVBAgMCFRlbmVyaWZlMRMwEQYDVQQHDApHcmFuYWRpbGxhMRMw
+EQYDVQQKDApTeW5waG9ueXRlMQwwCgYDVQQDDANCbGExJjAkBgkqhkiG9w0BCQEW
+F21hY2Nlc2NoQHN5bnBob255dGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAvDj30GptcuZcTnYZxSULMQqU1csp/54kRM7Emdpsf21es16rRoUO
+xXfvxGnbGgWb6F2lAG8Gmv7YoliOAliPpFTrmquGhAO/4pMGPsEWVa35/jPIZEpl
+oi3p9ouoDi3LIqgNFE3BlnW0Xf3ThnMw3P9pndA5+2NnJDKYKHp0tPZwGph6qgOf
+2nqp0UXZ9U5AqUdjxh7/rAec8NDmAz6TXPSDT2Tc7Xb5bUfWPf2IjxZvcdttjhlf
+8CyYVZPGWSWClc7R0yaydubnOH0uOYdQhwwQdGnYA+hca+esWoT8IejmD3oup0KE
+tRFFg7oCmD7H4v/r3jZb5XAvyqXTzzxwdwIDAQABo3AwbjAfBgNVHSMEGDAWgBS1
+MicRfZi+HsV8udux5h1T1EWDZTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAUBgNV
+HREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFJFXRB1VgjrDhxU287mxGisg+MzV
+MA0GCSqGSIb3DQEBCwUAA4IBAQCXBIKF8ylDhq80OxAe1EaRZKEwVKoVtoXEQLQy
+0voiIx00AjfMjUC16+8rAhDDLL5PWi56zonH/2hCfJyVec/oF059MghzCPAALv1G
+3koaU+4Tp9S17zLbmlExQN72LOGYs9hswWdrtqpWPeeLxzoYn+LMxANVzhBMz66y
+KwnwP/BFyeVKYIVTOZfH67j3PYCLizHCOoPOh0m9/ub8QIgFzKhYl4tWKRzJBug1
+wGYoMIbBasU9N4UoeeWC4OP6YA9MDmpy5D2xFDM0DKW7qYP/KsQSAgPbKKd0a//S
+xKv6DFpBeEhh6+d+MAHBYu+m7nTHeO90PCtVSohtTH7fSk0r
+-----END CERTIFICATE-----
diff --git a/examples/use_webtransport_with_server/server/cert.key b/examples/use_webtransport_with_server/server/cert.key
new file mode 100644
index 0000000..84069cb
--- /dev/null
+++ b/examples/use_webtransport_with_server/server/cert.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC8OPfQam1y5lxO
+dhnFJQsxCpTVyyn/niREzsSZ2mx/bV6zXqtGhQ7Fd+/EadsaBZvoXaUAbwaa/tii
+WI4CWI+kVOuaq4aEA7/ikwY+wRZVrfn+M8hkSmWiLen2i6gOLcsiqA0UTcGWdbRd
+/dOGczDc/2md0Dn7Y2ckMpgoenS09nAamHqqA5/aeqnRRdn1TkCpR2PGHv+sB5zw
+0OYDPpNc9INPZNztdvltR9Y9/YiPFm9x222OGV/wLJhVk8ZZJYKVztHTJrJ25uc4
+fS45h1CHDBB0adgD6Fxr56xahPwh6OYPei6nQoS1EUWDugKYPsfi/+veNlvlcC/K
+pdPPPHB3AgMBAAECggEADj4FSmjzNTGHJIy9MHS4HxLc5jyERgpSVj6LE9U6Rn4h
+H1N3hFOHJZwIsYUNBjAMdw228Yx1JH9KJyaqQDUxUU73sPFvsUeTWnKjk1YK+Zq7
+gueqLySOAjKVNImmwsPmTg4HR1UG4/quFjqhqdfHh8Fv3XgnGwWPhWaqqs1xTUwD
+DgatljfrjTt8Nx2RHtwgUQ8ZrAgrb4arQk6WjGqN4smg7n0oszmwmpRtB+9m6aNw
+CXULF+qxFSWVb/5fiV4B0D2US7aNHNcrSG4yg3FYgW1HsGdFhGhnUCAGpvrzenmm
+7bkijuQgkmcpd0+KMXM7rcayUCcW6wt2/ZTVL0BJ+QKBgQD09Vuymz5SBAkwbjPB
+YD4JJFl8ffhW4JiXRa2q5+BwKjesk2nBLUncC8Y+lL9T8tp4+NGGujKjd/EcM03r
+viq+Bh3ZZqo0WwFAQscQ9iHncf783JxSfkx60LrKpaSd7GQkvuifRizkZR0z3bde
+pMcueNtGTE3hQGxSvZYWHPCqWwKBgQDEtOs8k6yxDL/DfK/ldAL+AFbQ3tj+Zt22
+r6MBi/PuyGsvasXSyX/6n92a8kSC67dOwQyOwquTb/1nTMTErctD2gIyo8hjS9cw
++DdUkgaUxG0xcpWeHHTRHGwu70hqu0SRBw3AURj1NI4eohYt/gcoNEfZaFSgUprB
+sDNFGZ8VFQKBgDcpQVr5BpGlgwQ67MCxEYcxfk1AeLnnnbUC5dbEnI/lkd/02i28
+KxO4Ow5ApM0ctQHk1hoGt/yDt/Hnw7ZAfpOIARTBv7ZGgAOehgFVy9C4pPkAHNue
+wU4uzsFvh6BgaTS1IOEtBlLwSiEx3mcbqBbY9FfiOu9seHgxZSjZn4BdAoGALNLl
+P9qO4ZF8KTnCg1DaVbMSFWqSm/Yo07ZWOMYBggodkqKMDappBV1kjChkwEiibsnC
+6M0nd+NvJRjzRbYsuXt2QL/dq/LeSIRnZ1gXM9NG5puryGnHnNcTN+bC479ksn+e
+/JH+U/Hz6LsavsRCMUEoljwV/KqWJUjXhgl+nLkCgYA4D8lWtsMkKMh4d6iA4tR3
+93VdUYZG+nwJ0Cj6fgXCapWZ/ncRXlrUzD616W4poT+R9qG1dhNnUiaw81NsbjFj
+YK/sBHRTPnm8LbTGZ9WUOPcdQ+T2VNj0DLekiZ6RICazGZLTnj/RaJmJRb1w+ej1
+yNaquFcA6XZ3WLDYrrcvRA==
+-----END PRIVATE KEY-----
diff --git a/examples/use_window_scroll/src/main.rs b/examples/use_window_scroll/src/main.rs
index 627db6d..f659c54 100644
--- a/examples/use_window_scroll/src/main.rs
+++ b/examples/use_window_scroll/src/main.rs
@@ -16,20 +16,10 @@ fn Demo() -> impl IntoView {
document().body().unwrap().append_child(&div).unwrap();
view! {
-
- See scroll values in the lower right corner of the screen.
-
+
See scroll values in the lower right corner of the screen.
-
- Scroll value
-
-
- x:
- {move || format!("{:.1}", x())}
-
- y:
- {move || format!("{:.1}", y())}
-
+
Scroll value
+
x: {move || format!("{:.1}", x())} y: {move || format!("{:.1}", y())}
}
}
diff --git a/src/core/datetime.rs b/src/core/datetime.rs
new file mode 100644
index 0000000..66e72fb
--- /dev/null
+++ b/src/core/datetime.rs
@@ -0,0 +1,16 @@
+use cfg_if::cfg_if;
+
+/// SSR safe `Date.now()`.
+#[inline(always)]
+pub(crate) fn now() -> f64 {
+ cfg_if! { if #[cfg(feature = "ssr")] {
+ use std::time::{SystemTime, UNIX_EPOCH};
+
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("Time went backwards")
+ .as_millis() as f64
+ } else {
+ js_sys::Date::now()
+ }}
+}
diff --git a/src/core/direction.rs b/src/core/direction.rs
new file mode 100644
index 0000000..a6d1b23
--- /dev/null
+++ b/src/core/direction.rs
@@ -0,0 +1,41 @@
+/// Direction enum
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+pub enum Direction {
+ Top,
+ Bottom,
+ Left,
+ Right,
+}
+
+#[derive(Copy, Clone, Default, Debug)]
+/// Directions flags
+pub struct Directions {
+ pub left: bool,
+ pub right: bool,
+ pub top: bool,
+ pub bottom: bool,
+}
+
+impl Directions {
+ /// Returns the value of the provided direction
+ pub fn get_direction(&self, direction: Direction) -> bool {
+ match direction {
+ Direction::Top => self.top,
+ Direction::Bottom => self.bottom,
+ Direction::Left => self.left,
+ Direction::Right => self.right,
+ }
+ }
+
+ /// Sets the value of the provided direction
+ pub fn set_direction(mut self, direction: Direction, value: bool) -> Self {
+ match direction {
+ Direction::Top => self.top = value,
+ Direction::Bottom => self.bottom = value,
+ Direction::Left => self.left = value,
+ Direction::Right => self.right = value,
+ }
+
+ self
+ }
+}
diff --git a/src/core/element_maybe_signal.rs b/src/core/element_maybe_signal.rs
index 9d972a7..a100b7e 100644
--- a/src/core/element_maybe_signal.rs
+++ b/src/core/element_maybe_signal.rs
@@ -1,4 +1,5 @@
use crate::{UseDocument, UseWindow};
+use cfg_if::cfg_if;
use leptos::html::ElementDescriptor;
use leptos::*;
use std::marker::PhantomData;
@@ -177,7 +178,12 @@ where
E: From
+ 'static,
{
fn from(target: &'a str) -> Self {
- Self::Static(document().query_selector(target).unwrap_or_default())
+ cfg_if! { if #[cfg(feature = "ssr")] {
+ let _ = target;
+ Self::Static(None)
+ } else {
+ Self::Static(document().query_selector(target).unwrap_or_default())
+ }}
}
}
@@ -186,7 +192,7 @@ where
E: From + 'static,
{
fn from(target: String) -> Self {
- Self::Static(document().query_selector(&target).unwrap_or_default())
+ Self::from(target.as_str())
}
}
@@ -195,10 +201,15 @@ where
E: From + 'static,
{
fn from(signal: Signal) -> Self {
- Self::Dynamic(
- create_memo(move |_| document().query_selector(&signal.get()).unwrap_or_default())
- .into(),
- )
+ cfg_if! { if #[cfg(feature = "ssr")] {
+ let _ = signal;
+ Self::Dynamic(Signal::derive(|| None))
+ } else {
+ Self::Dynamic(
+ create_memo(move |_| document().query_selector(&signal.get()).unwrap_or_default())
+ .into(),
+ )
+ }}
}
}
diff --git a/src/core/elements_maybe_signal.rs b/src/core/elements_maybe_signal.rs
index 8f5eff3..2770d48 100644
--- a/src/core/elements_maybe_signal.rs
+++ b/src/core/elements_maybe_signal.rs
@@ -1,5 +1,6 @@
use crate::core::ElementMaybeSignal;
use crate::{UseDocument, UseWindow};
+use cfg_if::cfg_if;
use leptos::html::ElementDescriptor;
use leptos::*;
use std::marker::PhantomData;
@@ -178,17 +179,31 @@ where
E: From + 'static,
{
fn from(target: &'a str) -> Self {
- if let Ok(node_list) = document().query_selector_all(target) {
- let mut list = Vec::with_capacity(node_list.length() as usize);
- for i in 0..node_list.length() {
- let node = node_list.get(i).expect("checked the range");
- list.push(Some(node));
- }
+ cfg_if! { if #[cfg(feature = "ssr")] {
+ if let Ok(node_list) = document().query_selector_all(target) {
+ let mut list = Vec::with_capacity(node_list.length() as usize);
+ for i in 0..node_list.length() {
+ let node = node_list.get(i).expect("checked the range");
+ list.push(Some(node));
+ }
- Self::Static(list)
+ Self::Static(list)
+ } else {
+ Self::Static(vec![])
+ }
} else {
+ let _ = target;
Self::Static(vec![])
- }
+ }}
+ }
+}
+
+impl From for ElementsMaybeSignal
+where
+ E: From + 'static,
+{
+ fn from(target: String) -> Self {
+ Self::from(target.as_str())
}
}
@@ -197,21 +212,26 @@ where
E: From + 'static,
{
fn from(signal: Signal) -> Self {
- Self::Dynamic(
- create_memo(move |_| {
- if let Ok(node_list) = document().query_selector_all(&signal.get()) {
- let mut list = Vec::with_capacity(node_list.length() as usize);
- for i in 0..node_list.length() {
- let node = node_list.get(i).expect("checked the range");
- list.push(Some(node));
+ cfg_if! { if #[cfg(feature = "ssr")] {
+ Self::Dynamic(
+ create_memo(move |_| {
+ if let Ok(node_list) = document().query_selector_all(&signal.get()) {
+ let mut list = Vec::with_capacity(node_list.length() as usize);
+ for i in 0..node_list.length() {
+ let node = node_list.get(i).expect("checked the range");
+ list.push(Some(node));
+ }
+ list
+ } else {
+ vec![]
}
- list
- } else {
- vec![]
- }
- })
- .into(),
- )
+ })
+ .into(),
+ )
+ } else {
+ let _ = signal;
+ Self::Dynamic(Signal::derive(Vec::new))
+ }}
}
}
diff --git a/src/core/maybe_rw_signal.rs b/src/core/maybe_rw_signal.rs
index 6aad544..f1e8bac 100644
--- a/src/core/maybe_rw_signal.rs
+++ b/src/core/maybe_rw_signal.rs
@@ -1,4 +1,5 @@
use leptos::*;
+use std::fmt::Debug;
pub enum MaybeRwSignal
where
@@ -33,6 +34,16 @@ impl Default for MaybeRwSignal {
}
}
+impl Debug for MaybeRwSignal {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Static(t) => f.debug_tuple("Static").field(t).finish(),
+ Self::DynamicRw(r, w) => f.debug_tuple("DynamicRw").field(r).field(w).finish(),
+ Self::DynamicRead(s) => f.debug_tuple("DynamicRead").field(s).finish(),
+ }
+ }
+}
+
impl From> for MaybeRwSignal {
fn from(s: Signal) -> Self {
Self::DynamicRead(s)
diff --git a/src/core/mod.rs b/src/core/mod.rs
index 2a8c2e0..9abe354 100644
--- a/src/core/mod.rs
+++ b/src/core/mod.rs
@@ -1,17 +1,23 @@
mod connection_ready_state;
+mod datetime;
+mod direction;
mod element_maybe_signal;
mod elements_maybe_signal;
mod maybe_rw_signal;
mod pointer_type;
mod position;
mod size;
+mod ssr_safe_method;
mod storage;
pub use connection_ready_state::*;
+pub(crate) use datetime::*;
+pub use direction::*;
pub use element_maybe_signal::*;
pub use elements_maybe_signal::*;
pub use maybe_rw_signal::*;
pub use pointer_type::*;
pub use position::*;
pub use size::*;
+pub(crate) use ssr_safe_method::*;
pub use storage::*;
diff --git a/src/core/ssr_safe_method.rs b/src/core/ssr_safe_method.rs
new file mode 100644
index 0000000..9ffa513
--- /dev/null
+++ b/src/core/ssr_safe_method.rs
@@ -0,0 +1,19 @@
+macro_rules! impl_ssr_safe_method {
+ (
+ $(#[$attr:meta])*
+ $method:ident(&self$(, $p_name:ident: $p_ty:ty)*) -> $return_ty:ty
+ $(; $($post_fix:tt)+)?
+ ) => {
+ $(#[$attr])*
+ #[inline(always)]
+ pub fn $method(&self, $($p_name: $p_ty),*) -> $return_ty {
+ self.0.as_ref()
+ .map(
+ |w| w.$method($($p_name),*)
+ )
+ $($($post_fix)+)?
+ }
+ };
+}
+
+pub(crate) use impl_ssr_safe_method;
diff --git a/src/core/storage.rs b/src/core/storage.rs
index 72da8fa..8390217 100644
--- a/src/core/storage.rs
+++ b/src/core/storage.rs
@@ -2,7 +2,6 @@ use leptos::window;
use wasm_bindgen::JsValue;
/// Local or session storage or a custom store that is a `web_sys::Storage`.
-// #[doc(cfg(feature = "storage"))]
#[derive(Default)]
pub enum StorageType {
#[default]
diff --git a/src/lib.rs b/src/lib.rs
index 13ff243..1dd36ab 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,24 +1,16 @@
// #![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;
#[cfg(feature = "math")]
pub mod math;
-#[cfg(feature = "storage")]
pub mod storage;
pub mod utils;
cfg_if! { if #[cfg(web_sys_unstable_apis)] {
- mod use_element_size;
- mod use_resize_observer;
mod use_webtransport;
-
- pub use use_element_size::*;
- pub use use_resize_observer::*;
pub use use_webtransport::*;
}}
@@ -35,16 +27,19 @@ mod use_color_mode;
mod use_css_var;
mod use_cycle_list;
mod use_debounce_fn;
+mod use_display_media;
mod use_document;
mod use_document_visibility;
mod use_draggable;
mod use_drop_zone;
mod use_element_hover;
+mod use_element_size;
mod use_element_visibility;
mod use_event_listener;
mod use_favicon;
mod use_geolocation;
mod use_idle;
+mod use_infinite_scroll;
mod use_intersection_observer;
mod use_interval;
mod use_interval_fn;
@@ -55,11 +50,15 @@ mod use_mutation_observer;
mod use_preferred_contrast;
mod use_preferred_dark;
mod use_raf_fn;
+mod use_resize_observer;
mod use_scroll;
+mod use_service_worker;
+mod use_sorted;
mod use_supported;
mod use_throttle_fn;
mod use_timestamp;
mod use_to_string;
+mod use_web_notification;
mod use_websocket;
mod use_window;
mod use_window_focus;
@@ -83,16 +82,19 @@ pub use use_color_mode::*;
pub use use_css_var::*;
pub use use_cycle_list::*;
pub use use_debounce_fn::*;
+pub use use_display_media::*;
pub use use_document::*;
pub use use_document_visibility::*;
pub use use_draggable::*;
pub use use_drop_zone::*;
pub use use_element_hover::*;
+pub use use_element_size::*;
pub use use_element_visibility::*;
pub use use_event_listener::*;
pub use use_favicon::*;
pub use use_geolocation::*;
pub use use_idle::*;
+pub use use_infinite_scroll::*;
pub use use_intersection_observer::*;
pub use use_interval::*;
pub use use_interval_fn::*;
@@ -103,11 +105,15 @@ pub use use_mutation_observer::*;
pub use use_preferred_contrast::*;
pub use use_preferred_dark::*;
pub use use_raf_fn::*;
+pub use use_resize_observer::*;
pub use use_scroll::*;
+pub use use_service_worker::*;
+pub use use_sorted::*;
pub use use_supported::*;
pub use use_throttle_fn::*;
pub use use_timestamp::*;
pub use use_to_string::*;
+pub use use_web_notification::*;
pub use use_websocket::*;
pub use use_webtransport::*;
pub use use_window::*;
diff --git a/src/on_click_outside.rs b/src/on_click_outside.rs
index 1ac1cca..3d1a542 100644
--- a/src/on_click_outside.rs
+++ b/src/on_click_outside.rs
@@ -125,7 +125,7 @@ where
})
};
- let target = (target).into();
+ let target = target.into();
let listener = {
let should_listen = Rc::clone(&should_listen);
diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs
new file mode 100644
index 0000000..1a6a3a9
--- /dev/null
+++ b/src/storage/codec_json.rs
@@ -0,0 +1,151 @@
+use super::Codec;
+
+/// A codec for storing JSON messages that relies on [`serde_json`] to parse.
+///
+/// ## Example
+/// ```
+/// # use leptos::*;
+/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, JsonCodec};
+/// # use serde::{Deserialize, Serialize};
+/// #
+/// # pub fn Demo() -> impl IntoView {
+/// // Primitive types:
+/// let (get, set, remove) = use_local_storage::("my-key");
+///
+/// // Structs:
+/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)]
+/// pub struct MyState {
+/// pub hello: String,
+/// }
+/// let (get, set, remove) = use_local_storage::("my-struct-key");
+/// # view! { }
+/// # }
+/// ```
+///
+/// ## Versioning
+///
+/// If the JSON decoder fails, the storage hook will return `T::Default` dropping the stored JSON value. See [`Codec`](super::Codec) for general information on codec versioning.
+///
+/// ### Rely on serde
+/// This codec uses [`serde_json`] under the hood. A simple way to avoid complex versioning is to rely on serde's [field attributes](https://serde.rs/field-attrs.html) such as [`serde(default)`](https://serde.rs/field-attrs.html#default) and [`serde(rename = "...")`](https://serde.rs/field-attrs.html#rename).
+///
+/// ### String replacement
+/// Previous versions of leptos-use offered a `merge_defaults` fn to rewrite the encoded value. This is possible by wrapping the codec but should be avoided.
+///
+/// ```
+/// # use leptos::*;
+/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, Codec, JsonCodec};
+/// # use serde::{Deserialize, Serialize};
+/// #
+/// # pub fn Demo() -> impl IntoView {
+/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)]
+/// pub struct MyState {
+/// pub hello: String,
+/// pub greeting: String,
+/// }
+///
+/// #[derive(Clone, Default)]
+/// pub struct MyStateCodec();
+/// impl Codec for MyStateCodec {
+/// type Error = serde_json::Error;
+///
+/// fn encode(&self, val: &MyState) -> Result {
+/// serde_json::to_string(val)
+/// }
+///
+/// fn decode(&self, stored_value: String) -> Result {
+/// let default_value = MyState::default();
+/// let rewritten = if stored_value.contains(r#""greeting":"#) {
+/// stored_value
+/// } else {
+/// // add "greeting": "Hello" to the string
+/// stored_value.replace("}", &format!(r#""greeting": "{}"}}"#, default_value.greeting))
+/// };
+/// serde_json::from_str(&rewritten)
+/// }
+/// }
+///
+/// let (get, set, remove) = use_local_storage::("my-struct-key");
+/// # view! { }
+/// # }
+/// ```
+///
+/// ### Transform a `JsValue`
+/// A better alternative to string replacement might be to parse the JSON then transform the resulting `JsValue` before decoding it to to your struct again.
+///
+/// ```
+/// # use leptos::*;
+/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, Codec, JsonCodec};
+/// # use serde::{Deserialize, Serialize};
+/// # use serde_json::json;
+/// #
+/// # pub fn Demo() -> impl IntoView {
+/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)]
+/// pub struct MyState {
+/// pub hello: String,
+/// pub greeting: String,
+/// }
+///
+/// #[derive(Clone, Default)]
+/// pub struct MyStateCodec();
+/// impl Codec for MyStateCodec {
+/// type Error = serde_json::Error;
+///
+/// fn encode(&self, val: &MyState) -> Result {
+/// serde_json::to_string(val)
+/// }
+///
+/// fn decode(&self, stored_value: String) -> Result {
+/// let mut val: serde_json::Value = serde_json::from_str(&stored_value)?;
+/// // add "greeting": "Hello" to the object if it's missing
+/// if let Some(obj) = val.as_object_mut() {
+/// if !obj.contains_key("greeting") {
+/// obj.insert("greeting".to_string(), json!("Hello"));
+/// }
+/// serde_json::from_value(val)
+/// } else {
+/// Ok(MyState::default())
+/// }
+/// }
+/// }
+///
+/// let (get, set, remove) = use_local_storage::("my-struct-key");
+/// # view! { }
+/// # }
+/// ```
+#[derive(Clone, Default, PartialEq)]
+pub struct JsonCodec;
+
+impl Codec for JsonCodec {
+ type Error = serde_json::Error;
+
+ fn encode(&self, val: &T) -> Result {
+ serde_json::to_string(val)
+ }
+
+ fn decode(&self, str: String) -> Result {
+ serde_json::from_str(&str)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_json_codec() {
+ #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
+ struct Test {
+ s: String,
+ i: i32,
+ }
+ let t = Test {
+ s: String::from("party time 🎉"),
+ i: 42,
+ };
+ let codec = JsonCodec;
+ let enc = codec.encode(&t).unwrap();
+ let dec: Test = codec.decode(enc).unwrap();
+ assert_eq!(dec, t);
+ }
+}
diff --git a/src/storage/codec_prost.rs b/src/storage/codec_prost.rs
new file mode 100644
index 0000000..2311e28
--- /dev/null
+++ b/src/storage/codec_prost.rs
@@ -0,0 +1,79 @@
+use super::Codec;
+use base64::Engine;
+use thiserror::Error;
+
+/// A codec for storing ProtoBuf messages that relies on [`prost`] to parse.
+///
+/// [Protocol buffers](https://protobuf.dev/overview/) is a serialisation format useful for long-term storage. It provides semantics for versioning that are not present in JSON or other formats. [`prost`] is a Rust implementation of Protocol Buffers.
+///
+/// This codec uses [`prost`] to encode the message and then [`base64`](https://docs.rs/base64) to represent the bytes as a string.
+///
+/// ## Example
+/// ```
+/// # use leptos::*;
+/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, ProstCodec};
+/// #
+/// # pub fn Demo() -> impl IntoView {
+/// // Primitive types:
+/// let (get, set, remove) = use_local_storage::("my-key");
+///
+/// // Structs:
+/// #[derive(Clone, PartialEq, prost::Message)]
+/// pub struct MyState {
+/// #[prost(string, tag = "1")]
+/// pub hello: String,
+/// }
+/// let (get, set, remove) = use_local_storage::("my-struct-key");
+/// # view! { }
+/// # }
+/// ```
+///
+/// Note: we've defined and used the `prost` attribute here for brevity. Alternate usage would be to describe the message in a .proto file and use [`prost_build`](https://docs.rs/prost-build) to auto-generate the Rust code.
+#[derive(Clone, Default, PartialEq)]
+pub struct ProstCodec;
+
+#[derive(Error, Debug, PartialEq)]
+pub enum ProstCodecError {
+ #[error("failed to decode base64")]
+ DecodeBase64(base64::DecodeError),
+ #[error("failed to decode protobuf")]
+ DecodeProst(#[from] prost::DecodeError),
+}
+
+impl Codec for ProstCodec {
+ type Error = ProstCodecError;
+
+ fn encode(&self, val: &T) -> Result {
+ let buf = val.encode_to_vec();
+ Ok(base64::engine::general_purpose::STANDARD.encode(buf))
+ }
+
+ fn decode(&self, str: String) -> Result {
+ let buf = base64::engine::general_purpose::STANDARD
+ .decode(str)
+ .map_err(ProstCodecError::DecodeBase64)?;
+ T::decode(buf.as_slice()).map_err(ProstCodecError::DecodeProst)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_prost_codec() {
+ #[derive(Clone, PartialEq, prost::Message)]
+ struct Test {
+ #[prost(string, tag = "1")]
+ s: String,
+ #[prost(int32, tag = "2")]
+ i: i32,
+ }
+ let t = Test {
+ s: String::from("party time 🎉"),
+ i: 42,
+ };
+ let codec = ProstCodec;
+ assert_eq!(codec.decode(codec.encode(&t).unwrap()), Ok(t));
+ }
+}
diff --git a/src/storage/codec_string.rs b/src/storage/codec_string.rs
new file mode 100644
index 0000000..c7a8049
--- /dev/null
+++ b/src/storage/codec_string.rs
@@ -0,0 +1,44 @@
+use super::Codec;
+use std::str::FromStr;
+
+/// A codec for strings that relies on [`FromStr`] and [`ToString`] to parse.
+///
+/// This makes simple key / value easy to use for primitive types. It is also useful for encoding simple data structures without depending on serde.
+///
+/// ## Example
+/// ```
+/// # use leptos::*;
+/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, StringCodec};
+/// #
+/// # pub fn Demo() -> impl IntoView {
+/// let (get, set, remove) = use_local_storage::("my-key");
+/// # view! { }
+/// # }
+/// ```
+#[derive(Clone, Default, PartialEq)]
+pub struct StringCodec;
+
+impl Codec for StringCodec {
+ type Error = T::Err;
+
+ fn encode(&self, val: &T) -> Result {
+ Ok(val.to_string())
+ }
+
+ fn decode(&self, str: String) -> Result {
+ T::from_str(&str)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_string_codec() {
+ let s = String::from("party time 🎉");
+ let codec = StringCodec;
+ assert_eq!(codec.encode(&s), Ok(s.clone()));
+ assert_eq!(codec.decode(s.clone()), Ok(s));
+ }
+}
diff --git a/src/storage/mod.rs b/src/storage/mod.rs
index bc0844b..c5cd494 100644
--- a/src/storage/mod.rs
+++ b/src/storage/mod.rs
@@ -1,8 +1,18 @@
-mod shared;
+#[cfg(feature = "serde")]
+mod codec_json;
+#[cfg(feature = "prost")]
+mod codec_prost;
+mod codec_string;
mod use_local_storage;
mod use_session_storage;
mod use_storage;
+pub use crate::core::StorageType;
+#[cfg(feature = "serde")]
+pub use codec_json::*;
+#[cfg(feature = "prost")]
+pub use codec_prost::*;
+pub use codec_string::*;
pub use use_local_storage::*;
pub use use_session_storage::*;
pub use use_storage::*;
diff --git a/src/storage/shared.rs b/src/storage/shared.rs
deleted file mode 100644
index bd9ef93..0000000
--- a/src/storage/shared.rs
+++ /dev/null
@@ -1,96 +0,0 @@
-use crate::filter_builder_methods;
-use crate::storage::{StorageType, UseStorageError, UseStorageOptions};
-use crate::utils::{DebounceOptions, FilterOptions, ThrottleOptions};
-use default_struct_builder::DefaultBuilder;
-use leptos::*;
-use std::rc::Rc;
-
-macro_rules! use_specific_storage {
- ($(#[$outer:meta])*
- $storage_name:ident
- #[$simple_func:meta]
- ) => {
- paste! {
- $(#[$outer])*
- pub fn [](
- key: &str,
- defaults: D,
- ) -> (Signal, WriteSignal, impl Fn() + Clone)
- where
- for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
- D: Into>,
- T: Clone,
- {
- [](key, defaults, UseSpecificStorageOptions::default())
- }
-
- /// Version of
- #[$simple_func]
- /// that accepts [`UseSpecificStorageOptions`]. See
- #[$simple_func]
- /// for how to use.
- pub fn [](
- key: &str,
- defaults: D,
- options: UseSpecificStorageOptions,
- ) -> (Signal, WriteSignal, impl Fn() + Clone)
- where
- for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
- D: Into>,
- T: Clone,
- {
- use_storage_with_options(key, defaults, options.into_storage_options(StorageType::[<$storage_name:camel>]))
- }
- }
- };
-}
-
-pub(crate) use use_specific_storage;
-
-/// Options for [`use_local_storage_with_options`].
-// #[doc(cfg(feature = "storage"))]
-#[derive(DefaultBuilder)]
-pub struct UseSpecificStorageOptions {
- /// Listen to changes to this storage key from somewhere else. Defaults to true.
- listen_to_storage_changes: bool,
- /// If no value for the give key is found in the storage, write it. Defaults to true.
- write_defaults: bool,
- /// Takes the serialized (json) stored value and the default value and returns a merged version.
- /// Defaults to simply returning the stored value.
- merge_defaults: fn(&str, &T) -> String,
- /// Optional callback whenever an error occurs. The callback takes an argument of type [`UseStorageError`].
- on_error: Rc,
-
- /// Debounce or throttle the writing to storage whenever the value changes.
- filter: FilterOptions,
-}
-
-impl Default for UseSpecificStorageOptions {
- fn default() -> Self {
- Self {
- listen_to_storage_changes: true,
- write_defaults: true,
- merge_defaults: |stored_value, _default_value| stored_value.to_string(),
- on_error: Rc::new(|_| ()),
- filter: Default::default(),
- }
- }
-}
-
-impl UseSpecificStorageOptions {
- pub fn into_storage_options(self, storage_type: StorageType) -> UseStorageOptions {
- UseStorageOptions {
- storage_type,
- listen_to_storage_changes: self.listen_to_storage_changes,
- write_defaults: self.write_defaults,
- merge_defaults: self.merge_defaults,
- on_error: self.on_error,
- filter: self.filter,
- }
- }
-
- filter_builder_methods!(
- /// the serializing and storing into storage
- filter
- );
-}
diff --git a/src/storage/use_local_storage.rs b/src/storage/use_local_storage.rs
index 3ed7716..4479a4a 100644
--- a/src/storage/use_local_storage.rs
+++ b/src/storage/use_local_storage.rs
@@ -1,22 +1,36 @@
-use crate::core::MaybeRwSignal;
-use crate::storage::shared::{use_specific_storage, UseSpecificStorageOptions};
-use crate::storage::{use_storage_with_options, StorageType};
-use leptos::*;
-use paste::paste;
-use serde::{Deserialize, Serialize};
+use super::{use_storage_with_options, Codec, StorageType, UseStorageOptions};
+use leptos::signal_prelude::*;
-use_specific_storage!(
- /// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)
- ///
- /// ## Usage
- ///
- /// Please refer to [`use_storage`]
- ///
- /// ## See also
- ///
- /// * [`use_storage`]
- /// * [`use_session_storage`]
- // #[doc(cfg(feature = "storage"))]
- local
- /// [`use_local_storage`]
-);
+/// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
+///
+/// LocalStorage stores data in the browser with no expiration time. Access is given to all pages from the same origin (e.g., all pages from "https://example.com" share the same origin). While data doesn't expire the user can view, modify and delete all data stored. Browsers allow 5MB of data to be stored.
+///
+/// This is contrast to [`use_session_storage`] which clears data when the page session ends and is not shared.
+///
+/// ## Usage
+/// See [`use_storage`] for more details on how to use.
+pub fn use_local_storage(
+ key: impl AsRef,
+) -> (Signal, WriteSignal, impl Fn() + Clone)
+where
+ T: Clone + Default + PartialEq,
+ C: Codec + Default,
+{
+ use_storage_with_options(
+ StorageType::Local,
+ key,
+ UseStorageOptions::::default(),
+ )
+}
+
+/// Accepts [`UseStorageOptions`]. See [`use_local_storage`] for details.
+pub fn use_local_storage_with_options(
+ key: impl AsRef,
+ options: UseStorageOptions,
+) -> (Signal, WriteSignal, impl Fn() + Clone)
+where
+ T: Clone + PartialEq,
+ C: Codec,
+{
+ use_storage_with_options(StorageType::Local, key, options)
+}
diff --git a/src/storage/use_session_storage.rs b/src/storage/use_session_storage.rs
index dca9b95..127e457 100644
--- a/src/storage/use_session_storage.rs
+++ b/src/storage/use_session_storage.rs
@@ -1,22 +1,36 @@
-use crate::core::MaybeRwSignal;
-use crate::storage::shared::{use_specific_storage, UseSpecificStorageOptions};
-use crate::storage::{use_storage_with_options, StorageType};
-use leptos::*;
-use paste::paste;
-use serde::{Deserialize, Serialize};
+use super::{use_storage_with_options, Codec, StorageType, UseStorageOptions};
+use leptos::signal_prelude::*;
-use_specific_storage!(
- /// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)
- ///
- /// ## Usage
- ///
- /// Please refer to [`use_storage`]
- ///
- /// ## See also
- ///
- /// * [`use_storage`]
- /// * [`use_local_storage`]
- // #[doc(cfg(feature = "storage"))]
- session
- /// [`use_session_storage`]
-);
+/// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage).
+///
+/// SessionStorages stores data in the browser that is deleted when the page session ends. A page session ends when the browser closes the tab. Data is not shared between pages. While data doesn't expire the user can view, modify and delete all data stored. Browsers allow 5MB of data to be stored.
+///
+/// Use [`use_local_storage`] to store data that is shared amongst all pages with the same origin and persists between page sessions.
+///
+/// ## Usage
+/// See [`use_storage`] for more details on how to use.
+pub fn use_session_storage(
+ key: impl AsRef,
+) -> (Signal, WriteSignal, impl Fn() + Clone)
+where
+ T: Clone + Default + PartialEq,
+ C: Codec + Default,
+{
+ use_storage_with_options(
+ StorageType::Session,
+ key,
+ UseStorageOptions::::default(),
+ )
+}
+
+/// Accepts [`UseStorageOptions`]. See [`use_session_storage`] for details.
+pub fn use_session_storage_with_options(
+ key: impl AsRef,
+ options: UseStorageOptions,
+) -> (Signal, WriteSignal, impl Fn() + Clone)
+where
+ T: Clone + PartialEq,
+ C: Codec,
+{
+ use_storage_with_options(StorageType::Session, key, options)
+}
diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs
index 7122882..3c66fce 100644
--- a/src/storage/use_storage.rs
+++ b/src/storage/use_storage.rs
@@ -1,26 +1,17 @@
-#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports, dead_code))]
-
-use crate::core::MaybeRwSignal;
-use crate::utils::FilterOptions;
+use crate::storage::StringCodec;
use crate::{
- filter_builder_methods, use_event_listener, watch_pausable_with_options, DebounceOptions,
- ThrottleOptions, WatchOptions, WatchPausableReturn,
+ core::{MaybeRwSignal, StorageType},
+ utils::FilterOptions,
};
use cfg_if::cfg_if;
-use default_struct_builder::DefaultBuilder;
-use js_sys::Reflect;
use leptos::*;
-use serde::{Deserialize, Serialize};
-use serde_json::Error;
use std::rc::Rc;
-use std::time::Duration;
-use wasm_bindgen::{JsCast, JsValue};
+use thiserror::Error;
+use wasm_bindgen::JsValue;
-pub use crate::core::StorageType;
+const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
-const CUSTOM_STORAGE_EVENT_NAME: &str = "leptos-use-storage";
-
-/// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) / [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage).
+/// Reactive [Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage).
///
/// ## Demo
///
@@ -28,471 +19,392 @@ const CUSTOM_STORAGE_EVENT_NAME: &str = "leptos-use-storage";
///
/// ## Usage
///
-/// It returns a triplet `(read_signal, write_signal, delete_from_storage_func)` of type `(ReadSignal, WriteSignal, Fn())`.
+/// Pass a [`StorageType`] to determine the kind of key-value browser storage to use. The specified key is where data is stored. All values are stored as UTF-16 strings which is then encoded and decoded via the given [`Codec`]. This value is synced with other calls using the same key on the smae page and across tabs for local storage. See [`UseStorageOptions`] to see how behaviour can be further customised.
///
-/// Values are (de-)serialized to/from JSON using [`serde`](https://serde.rs/).
+/// See [`Codec`] for more details on how to handle versioning--dealing with data that can outlast your code.
+///
+/// Returns a triplet `(read_signal, write_signal, delete_from_storage_fn)`.
+///
+/// ## Example
///
/// ```
/// # use leptos::*;
-/// # use leptos_use::storage::{StorageType, use_storage, use_storage_with_options, UseStorageOptions};
+/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec};
/// # use serde::{Deserialize, Serialize};
/// #
-/// #[derive(Serialize, Deserialize, Clone)]
-/// pub struct MyState {
-/// pub hello: String,
-/// pub greeting: String,
-/// }
-///
/// # pub fn Demo() -> impl IntoView {
-/// // bind struct. Must be serializable.
-/// let (state, set_state, _) = use_storage(
-/// "my-state",
-/// MyState {
-/// hello: "hi".to_string(),
-/// greeting: "Hello".to_string()
-/// },
-/// ); // returns Signal
+/// // Binds a struct:
+/// let (state, set_state, _) = use_local_storage::("my-state");
///
-/// // bind bool.
-/// let (flag, set_flag, remove_flag) = use_storage("my-flag", true); // returns Signal
+/// // Binds a bool, stored as a string:
+/// let (flag, set_flag, remove_flag) = use_session_storage::("my-flag");
///
-/// // bind number
-/// let (count, set_count, _) = use_storage("my-count", 0); // returns Signal
+/// // Binds a number, stored as a string:
+/// let (count, set_count, _) = use_session_storage::("my-count");
+/// // Binds a number, stored in JSON:
+/// let (count, set_count, _) = use_session_storage::("my-count-kept-in-js");
///
-/// // bind string with SessionStorage
-/// let (id, set_id, _) = use_storage_with_options(
+/// // Bind string with SessionStorage stored in ProtoBuf format:
+/// let (id, set_id, _) = use_storage_with_options::(
+/// StorageType::Session,
/// "my-id",
-/// "some_string_id".to_string(),
-/// UseStorageOptions::default().storage_type(StorageType::Session),
+/// UseStorageOptions::default(),
/// );
/// # view! { }
/// # }
-/// ```
///
-/// ## Merge Defaults
-///
-/// By default, [`use_storage`] will use the value from storage if it is present and ignores the default value.
-/// Be aware that when you add more properties to the default value, the key might be `None`
-/// (in the case of an `Option` field) if client's storage does not have that key
-/// or deserialization might fail altogether.
-///
-/// Let's say you had a struct `MyState` that has been saved to storage
-///
-/// ```ignore
-/// #[derive(Serialize, Deserialize, Clone)]
-/// struct MyState {
-/// hello: String,
-/// }
-///
-/// let (state, .. ) = use_storage("my-state", MyState { hello: "hello" });
-/// ```
-///
-/// Now, in a newer version you added a field `greeting` to `MyState`.
-///
-/// ```ignore
-/// #[derive(Serialize, Deserialize, Clone)]
-/// struct MyState {
-/// hello: String,
-/// greeting: String,
-/// }
-///
-/// let (state, .. ) = use_storage(
-/// "my-state",
-/// MyState { hello: "hi", greeting: "whatsup" },
-/// ); // fails to deserialize -> default value
-/// ```
-///
-/// This will fail to deserialize the stored string `{"hello": "hello"}` because it has no field `greeting`.
-/// Hence it just uses the new default value provided and the previously saved value is lost.
-///
-/// To mitigate that you can provide a `merge_defaults` option. This is a pure function pointer
-/// that takes the serialized (to json) stored value and the default value as arguments
-/// and should return the serialized merged value.
-///
-/// ```
-/// # use leptos::*;
-/// # use leptos_use::storage::{use_storage_with_options, UseStorageOptions};
-/// # use serde::{Deserialize, Serialize};
-/// #
-/// #[derive(Serialize, Deserialize, Clone)]
+/// // Data stored in JSON must implement Serialize, Deserialize:
+/// #[derive(Serialize, Deserialize, Clone, PartialEq)]
/// pub struct MyState {
/// pub hello: String,
/// pub greeting: String,
/// }
-/// #
-/// # pub fn Demo() -> impl IntoView {
-/// let (state, set_state, _) = use_storage_with_options(
-/// "my-state",
-/// MyState {
-/// hello: "hi".to_string(),
-/// greeting: "Hello".to_string()
-/// },
-/// UseStorageOptions::::default().merge_defaults(|stored_value, default_value| {
-/// if stored_value.contains(r#""greeting":"#) {
-/// stored_value.to_string()
-/// } else {
-/// // add "greeting": "Hello" to the string
-/// stored_value.replace("}", &format!(r#""greeting": "{}"}}"#, default_value.greeting))
+///
+/// // Default can be used to implement intial or deleted values.
+/// // You can also use a signal via UseStorageOptions::default_value`
+/// impl Default for MyState {
+/// fn default() -> Self {
+/// Self {
+/// hello: "hi".to_string(),
+/// greeting: "Hello".to_string()
/// }
-/// }),
-/// );
-/// #
-/// # view! { }
-/// # }
+/// }
+/// }
/// ```
-///
-/// ## Filter Storage Write
-///
-/// 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(default)` and an empty remove function.
-///
-/// ## See also
-///
-/// * [`use_local_storage`]
-/// * [`use_session_storage`]
-// #[doc(cfg(feature = "storage"))]
-pub fn use_storage(key: &str, defaults: D) -> (Signal, WriteSignal, impl Fn() + Clone)
-where
- for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
- D: Into>,
- T: Clone,
-{
- use_storage_with_options(key, defaults, UseStorageOptions::default())
+#[inline(always)]
+pub fn use_storage(
+ storage_type: StorageType,
+ key: impl AsRef,
+) -> (Signal, WriteSignal, impl Fn() + Clone) {
+ use_storage_with_options::(storage_type, key, UseStorageOptions::default())
}
-/// Version of [`use_storage`] that accepts [`UseStorageOptions`]. See [`use_storage`] for how to use.
-// #[doc(cfg(feature = "storage"))]
-pub fn use_storage_with_options(
- key: &str,
- defaults: D,
- options: UseStorageOptions,
+/// Version of [`use_storage`] that accepts [`UseStorageOptions`].
+pub fn use_storage_with_options(
+ storage_type: StorageType,
+ key: impl AsRef,
+ options: UseStorageOptions,
) -> (Signal, WriteSignal, impl Fn() + Clone)
where
- for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
- D: Into>,
- T: Clone,
+ T: Clone + PartialEq,
+ C: Codec,
{
- let defaults = defaults.into();
-
let UseStorageOptions {
- storage_type,
- listen_to_storage_changes,
- write_defaults,
- merge_defaults,
+ codec,
on_error,
+ listen_to_storage_changes,
+ initial_value,
filter,
} = options;
- let (data, set_data) = defaults.into_signal();
-
- let raw_init = data.get_untracked();
+ let (data, set_data) = initial_value.into_signal();
+ let default = data.get_untracked();
cfg_if! { if #[cfg(feature = "ssr")] {
- let remove: Rc = Rc::new(|| {});
+ let _ = codec;
+ let _ = on_error;
+ let _ = listen_to_storage_changes;
+ let _ = filter;
+ let _ = storage_type;
+ let _ = key;
+ let _ = INTERNAL_STORAGE_EVENT;
+
+
+ let remove = move || {
+ set_data.set(default.clone());
+ };
+
+ (data.into(), set_data, remove)
} else {
- let storage = storage_type.into_storage();
+ use crate::{use_event_listener, use_window, watch_with_options, WatchOptions};
- let remove: Rc = match storage {
- Ok(Some(storage)) => {
- let write = {
- let on_error = on_error.clone();
- let storage = storage.clone();
- let key = key.to_string();
+ // Get storage API
+ let storage = storage_type
+ .into_storage()
+ .map_err(UseStorageError::StorageNotAvailable)
+ .and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone));
+ let storage = handle_error(&on_error, storage);
- Rc::new(move |v: &T| {
- match serde_json::to_string(&v) {
- Ok(ref serialized) => match storage.get_item(&key) {
- Ok(old_value) => {
- if old_value.as_ref() != Some(serialized) {
- if let Err(e) = storage.set_item(&key, serialized) {
- on_error(UseStorageError::StorageAccessError(e));
- } else {
- let mut event_init = web_sys::CustomEventInit::new();
- event_init.detail(
- &StorageEventDetail {
- key: Some(key.clone()),
- old_value,
- new_value: Some(serialized.clone()),
- storage_area: Some(storage.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"),
- );
- }
- }
- }
- Err(e) => {
- on_error.clone()(UseStorageError::StorageAccessError(e));
- }
- },
- Err(e) => {
- on_error.clone()(UseStorageError::SerializationError(e));
- }
- }
- })
- };
-
- let read = {
- let storage = storage.clone();
- let on_error = on_error.clone();
- let key = key.to_string();
- let raw_init = raw_init.clone();
-
- Rc::new(
- move |event_detail: Option| -> Option {
- let serialized_init = match serde_json::to_string(&raw_init) {
- Ok(serialized) => Some(serialized),
- Err(e) => {
- on_error.clone()(UseStorageError::DefaultSerializationError(e));
- None
- }
- };
-
- let raw_value = if let Some(event_detail) = event_detail {
- event_detail.new_value
- } else {
- match storage.get_item(&key) {
- Ok(raw_value) => match raw_value {
- Some(raw_value) => Some(merge_defaults(&raw_value, &raw_init)),
- None => serialized_init.clone(),
- },
- Err(e) => {
- on_error.clone()(UseStorageError::StorageAccessError(e));
- None
- }
- }
- };
-
- match raw_value {
- Some(raw_value) => match serde_json::from_str(&raw_value) {
- Ok(v) => Some(v),
- Err(e) => {
- on_error.clone()(UseStorageError::SerializationError(e));
- None
- }
- },
- None => {
- if let Some(serialized_init) = &serialized_init {
- if write_defaults {
- if let Err(e) = storage.set_item(&key, serialized_init) {
- on_error(UseStorageError::StorageAccessError(e));
- }
- }
- }
-
- Some(raw_init.clone())
- }
- }
- },
- )
- };
-
- let WatchPausableReturn {
- pause: pause_watch,
- resume: resume_watch,
- ..
- } = watch_pausable_with_options(
- move || data.get(),
- move |data, _, _| Rc::clone(&write)(data),
- WatchOptions::default().filter(filter),
- );
-
- let update = {
- let key = key.to_string();
- let storage = storage.clone();
- let raw_init = raw_init.clone();
-
- Rc::new(move |event_detail: Option| {
- if let Some(event_detail) = &event_detail {
- if event_detail.storage_area != Some(storage.clone()) {
- return;
- }
-
- match &event_detail.key {
- None => {
- set_data.set(raw_init.clone());
- return;
- }
- Some(event_key) => {
- if event_key != &key {
- return;
- }
- }
- };
- }
-
- pause_watch();
-
- if let Some(value) = read(event_detail.clone()) {
- set_data.set(value);
- }
-
- if event_detail.is_some() {
- // use timeout to avoid infinite loop
- let resume = resume_watch.clone();
- let _ = set_timeout_with_handle(resume, Duration::ZERO);
- } else {
- resume_watch();
- }
- })
- };
-
- let update_from_custom_event = {
- let update = Rc::clone(&update);
-
- move |event: web_sys::CustomEvent| {
- let update = Rc::clone(&update);
- queue_microtask(move || update(Some(event.into())))
- }
- };
-
- let update_from_storage_event = {
- let update = Rc::clone(&update);
-
- move |event: web_sys::StorageEvent| update(Some(event.into()))
- };
-
- if listen_to_storage_changes {
- let _ = use_event_listener(window(), ev::storage, update_from_storage_event);
- let _ = use_event_listener(
- window(),
- ev::Custom::new(CUSTOM_STORAGE_EVENT_NAME),
- update_from_custom_event,
- );
- }
-
- update(None);
-
- let k = key.to_string();
-
- Rc::new(move || {
- let _ = storage.remove_item(&k);
+ // Schedules a storage event microtask. Uses a queue to avoid re-entering the runtime
+ let dispatch_storage_event = {
+ let key = key.as_ref().to_owned();
+ let on_error = on_error.to_owned();
+ move || {
+ let key = key.to_owned();
+ let on_error = on_error.to_owned();
+ queue_microtask(move || {
+ // Note: we cannot construct a full StorageEvent so we _must_ rely on a custom event
+ let mut custom = web_sys::CustomEventInit::new();
+ custom.detail(&JsValue::from_str(&key));
+ let result = window()
+ .dispatch_event(
+ &web_sys::CustomEvent::new_with_event_init_dict(
+ INTERNAL_STORAGE_EVENT,
+ &custom,
+ )
+ .expect("failed to create custom storage event"),
+ )
+ .map_err(UseStorageError::NotifyItemChangedFailed);
+ let _ = handle_error(&on_error, result);
})
}
- Err(e) => {
- on_error(UseStorageError::NoStorage(e));
- Rc::new(move || {})
- }
- _ => {
- // do nothing
- Rc::new(move || {})
+ };
+
+ // Fetches direct from browser storage and fills set_data if changed (memo)
+ let fetch_from_storage = {
+ let storage = storage.to_owned();
+ let codec = codec.to_owned();
+ let key = key.as_ref().to_owned();
+ let on_error = on_error.to_owned();
+ move || {
+ let fetched = storage
+ .to_owned()
+ .and_then(|storage| {
+ // Get directly from storage
+ let result = storage
+ .get_item(&key)
+ .map_err(UseStorageError::GetItemFailed);
+ handle_error(&on_error, result)
+ })
+ .unwrap_or_default() // Drop handled Err(())
+ .map(|encoded| {
+ // Decode item
+ let result = codec
+ .decode(encoded)
+ .map_err(UseStorageError::ItemCodecError);
+ handle_error(&on_error, result)
+ })
+ .transpose()
+ .unwrap_or_default(); // Drop handled Err(())
+
+ match fetched {
+ Some(value) => {
+ // Replace data if changed
+ if value != data.get_untracked() {
+ set_data.set(value)
+ }
+ }
+
+ // Revert to default
+ None => set_data.set(default.clone()),
+ };
}
};
+
+ // Fetch initial value
+ fetch_from_storage();
+
+ // Fires when storage needs to be fetched
+ let notify = create_trigger();
+
+ // Refetch from storage. Keeps track of how many times we've been notified. Does not increment for calls to set_data
+ let notify_id = create_memo::(move |prev| {
+ notify.track();
+ match prev {
+ None => 1, // Avoid async fetch of initial value
+ Some(prev) => {
+ fetch_from_storage();
+ prev + 1
+ }
+ }
+ });
+
+ // Set item on internal (non-event) page changes to the data signal
+ {
+ let storage = storage.to_owned();
+ let codec = codec.to_owned();
+ let key = key.as_ref().to_owned();
+ let on_error = on_error.to_owned();
+ let dispatch_storage_event = dispatch_storage_event.to_owned();
+ let _ = watch_with_options(
+ move || (notify_id.get(), data.get()),
+ move |(id, value), prev, _| {
+ // Skip setting storage on changes from external events. The ID will change on external events.
+ if prev.map(|(prev_id, _)| *prev_id != *id).unwrap_or_default() {
+ return;
+ }
+
+ if let Ok(storage) = &storage {
+ // Encode value
+ let result = codec
+ .encode(value)
+ .map_err(UseStorageError::ItemCodecError)
+ .and_then(|enc_value| {
+ // Set storage -- sends a global event
+ storage
+ .set_item(&key, &enc_value)
+ .map_err(UseStorageError::SetItemFailed)
+ });
+ let result = handle_error(&on_error, result);
+ // Send internal storage event
+ if result.is_ok() {
+ dispatch_storage_event();
+ }
+ }
+ },
+ WatchOptions::default().filter(filter),
+ );
+ }
+
+ if listen_to_storage_changes {
+ let check_key = key.as_ref().to_owned();
+ // Listen to global storage events
+ let _ = use_event_listener(use_window(), leptos::ev::storage, move |ev| {
+ let ev_key = ev.key();
+ // Key matches or all keys deleted (None)
+ if ev_key == Some(check_key.clone()) || ev_key.is_none() {
+ notify.notify()
+ }
+ });
+ // Listen to internal storage events
+ let check_key = key.as_ref().to_owned();
+ let _ = use_event_listener(
+ use_window(),
+ ev::Custom::new(INTERNAL_STORAGE_EVENT),
+ move |ev: web_sys::CustomEvent| {
+ if Some(check_key.clone()) == ev.detail().as_string() {
+ notify.notify()
+ }
+ },
+ );
+ };
+
+ // Remove from storage fn
+ let remove = {
+ let key = key.as_ref().to_owned();
+ move || {
+ let _ = storage.as_ref().map(|storage| {
+ // Delete directly from storage
+ let result = storage
+ .remove_item(&key)
+ .map_err(UseStorageError::RemoveItemFailed);
+ let _ = handle_error(&on_error, result);
+ notify.notify();
+ dispatch_storage_event();
+ });
+ }
+ };
+
+ (data, set_data, remove)
}}
-
- (data, set_data, move || remove())
}
-#[derive(Clone)]
-pub struct StorageEventDetail {
- pub key: Option,
- pub old_value: Option,
- pub new_value: Option,
- pub storage_area: Option,
+/// Session handling errors returned by [`use_storage_with_options`].
+#[derive(Error, Debug)]
+pub enum UseStorageError {
+ #[error("storage not available")]
+ StorageNotAvailable(JsValue),
+ #[error("storage not returned from window")]
+ StorageReturnedNone,
+ #[error("failed to get item")]
+ GetItemFailed(JsValue),
+ #[error("failed to set item")]
+ SetItemFailed(JsValue),
+ #[error("failed to delete item")]
+ RemoveItemFailed(JsValue),
+ #[error("failed to notify item changed")]
+ NotifyItemChangedFailed(JsValue),
+ #[error("failed to encode / decode item value")]
+ ItemCodecError(Err),
}
-impl From for StorageEventDetail {
- fn from(event: web_sys::StorageEvent) -> Self {
- Self {
- key: event.key(),
- old_value: event.old_value(),
- new_value: event.new_value(),
- storage_area: event.storage_area(),
- }
- }
+/// Options for use with [`use_local_storage_with_options`], [`use_session_storage_with_options`] and [`use_storage_with_options`].
+pub struct UseStorageOptions> {
+ // Translates to and from UTF-16 strings
+ codec: C,
+ // Callback for when an error occurs
+ on_error: Rc)>,
+ // Whether to continuously listen to changes from browser storage
+ listen_to_storage_changes: bool,
+ // Initial value to use when the storage key is not set
+ initial_value: MaybeRwSignal,
+ // Debounce or throttle the writing to storage whenever the value changes
+ filter: FilterOptions,
}
-impl From for StorageEventDetail {
- fn from(event: web_sys::CustomEvent) -> Self {
- let detail = event.detail();
- Self {
- key: get_optional_string(&detail, "key"),
- old_value: get_optional_string(&detail, "oldValue"),
- new_value: get_optional_string(&detail, "newValue"),
- storage_area: Reflect::get(&detail, &"storageArea".into())
- .map(|v| v.dyn_into::().ok())
- .unwrap_or_default(),
- }
- }
+/// A codec for encoding and decoding values to and from UTF-16 strings. These strings are intended to be stored in browser storage.
+///
+/// ## Versioning
+///
+/// Versioning is the process of handling long-term data that can outlive our code.
+///
+/// For example we could have a settings struct whose members change over time. We might eventually add timezone support and we might then remove support for a thousand separator on numbers. Each change results in a new possible version of the stored data. If we stored these settings in browser storage we would need to handle all possible versions of the data format that can occur. If we don't offer versioning then all settings could revert to the default every time we encounter an old format.
+///
+/// How best to handle versioning depends on the codec involved:
+///
+/// - The [`StringCodec`](super::StringCodec) can avoid versioning entirely by keeping to privimitive types. In our example above, we could have decomposed the settings struct into separate timezone and number separator fields. These would be encoded as strings and stored as two separate key-value fields in the browser rather than a single field. If a field is missing then the value intentionally would fallback to the default without interfering with the other field.
+///
+/// - The [`ProstCodec`](super::ProstCodec) uses [Protocol buffers](https://protobuf.dev/overview/) designed to solve the problem of long-term storage. It provides semantics for versioning that are not present in JSON or other formats.
+///
+/// - The [`JsonCodec`](super::JsonCodec) stores data as JSON. We can then rely on serde or by providing our own manual version handling. See the codec for more details.
+pub trait Codec: Clone + 'static {
+ /// The error type returned when encoding or decoding fails.
+ type Error;
+ /// Encodes a value to a UTF-16 string.
+ fn encode(&self, val: &T) -> Result;
+ /// Decodes a UTF-16 string to a value. Should be able to decode any string encoded by [`encode`].
+ fn decode(&self, str: String) -> Result;
}
-impl From for JsValue {
- fn from(event: StorageEventDetail) -> Self {
- let obj = js_sys::Object::new();
-
- let _ = Reflect::set(&obj, &"key".into(), &event.key.into());
- let _ = Reflect::set(&obj, &"oldValue".into(), &event.old_value.into());
- let _ = Reflect::set(&obj, &"newValue".into(), &event.new_value.into());
- let _ = Reflect::set(&obj, &"storageArea".into(), &event.storage_area.into());
-
- obj.into()
- }
+/// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling.
+#[cfg(not(feature = "ssr"))]
+fn handle_error(
+ on_error: &Rc)>,
+ result: Result>,
+) -> Result {
+ result.map_err(|err| (on_error)(err))
}
-fn get_optional_string(v: &JsValue, key: &str) -> Option {
- Reflect::get(v, &key.into())
- .map(|v| v.as_string())
- .unwrap_or_default()
-}
-
-/// Error type for use_storage_with_options
-// #[doc(cfg(feature = "storage"))]
-pub enum UseStorageError {
- NoStorage(JsValue),
- StorageAccessError(JsValue),
- CustomStorageAccessError(E),
- SerializationError(Error),
- DefaultSerializationError(Error),
-}
-
-/// Options for [`use_storage_with_options`].
-// #[doc(cfg(feature = "storage"))]
-#[derive(DefaultBuilder)]
-pub struct UseStorageOptions {
- /// Type of storage. Can be `Local` (default), `Session` or `Custom(web_sys::Storage)`
- pub(crate) storage_type: StorageType,
- /// Listen to changes to this storage key from somewhere else. Defaults to true.
- pub(crate) listen_to_storage_changes: bool,
- /// If no value for the give key is found in the storage, write it. Defaults to true.
- pub(crate) write_defaults: bool,
- /// Takes the serialized (json) stored value and the default value and returns a merged version.
- /// Defaults to simply returning the stored value.
- pub(crate) merge_defaults: fn(&str, &T) -> String,
- /// Optional callback whenever an error occurs. The callback takes an argument of type [`UseStorageError`].
- pub(crate) on_error: Rc,
-
- /// Debounce or throttle the writing to storage whenever the value changes.
- pub(crate) filter: FilterOptions,
-}
-
-impl Default for UseStorageOptions {
+impl + Default> Default for UseStorageOptions {
fn default() -> Self {
Self {
- storage_type: Default::default(),
+ codec: C::default(),
+ on_error: Rc::new(|_err| ()),
listen_to_storage_changes: true,
- write_defaults: true,
- merge_defaults: |stored_value, _default_value| stored_value.to_string(),
- on_error: Rc::new(|_| ()),
- filter: Default::default(),
+ initial_value: MaybeRwSignal::default(),
+ filter: FilterOptions::default(),
}
}
}
-impl UseStorageOptions {
- filter_builder_methods!(
- /// the serializing and storing into storage
- filter
- );
+impl> UseStorageOptions {
+ /// Sets the codec to use for encoding and decoding values to and from UTF-16 strings.
+ pub fn codec(self, codec: impl Into) -> Self {
+ Self {
+ codec: codec.into(),
+ ..self
+ }
+ }
+
+ /// Optional callback whenever an error occurs.
+ pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self {
+ Self {
+ on_error: Rc::new(on_error),
+ ..self
+ }
+ }
+
+ /// Listen to changes to this storage key from browser and page events. Defaults to true.
+ pub fn listen_to_storage_changes(self, listen_to_storage_changes: bool) -> Self {
+ Self {
+ listen_to_storage_changes,
+ ..self
+ }
+ }
+
+ /// Initial value to use when the storage key is not set. Note that this value is read once on creation of the storage hook and not updated again. Accepts a signal and defaults to `T::default()`.
+ pub fn initial_value(self, initial: impl Into>) -> Self {
+ Self {
+ initial_value: initial.into(),
+ ..self
+ }
+ }
+
+ /// Debounce or throttle the writing to storage whenever the value changes.
+ pub fn filter(self, filter: impl Into) -> Self {
+ Self {
+ filter: filter.into(),
+ ..self
+ }
+ }
}
diff --git a/src/use_active_element.rs b/src/use_active_element.rs
index 4a671b0..76da95a 100644
--- a/src/use_active_element.rs
+++ b/src/use_active_element.rs
@@ -1,6 +1,6 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
-use crate::{use_document, use_event_listener_with_options, UseEventListenerOptions};
+use crate::{use_document, use_event_listener_with_options, use_window, UseEventListenerOptions};
use leptos::ev::{blur, focus};
use leptos::html::{AnyElement, ToHtmlElement};
use leptos::*;
@@ -45,7 +45,7 @@ pub fn use_active_element() -> Signal>> {
let listener_options = UseEventListenerOptions::default().capture(true);
let _ = use_event_listener_with_options(
- window(),
+ use_window(),
blur,
move |event| {
if event.related_target().is_some() {
@@ -58,7 +58,7 @@ pub fn use_active_element() -> Signal >> {
);
let _ = use_event_listener_with_options(
- window(),
+ use_window(),
focus,
move |_| {
set_active_element.update(|el| *el = get_active_element());
diff --git a/src/use_breakpoints.rs b/src/use_breakpoints.rs
index 578c15f..d05840f 100644
--- a/src/use_breakpoints.rs
+++ b/src/use_breakpoints.rs
@@ -1,4 +1,4 @@
-use crate::use_media_query;
+use crate::{use_media_query, use_window};
use leptos::logging::error;
use leptos::*;
use paste::paste;
@@ -185,7 +185,7 @@ macro_rules! impl_cmp_reactively {
impl UseBreakpointsReturn {
fn match_(query: &str) -> bool {
- if let Ok(Some(query_list)) = window().match_media(query) {
+ if let Ok(Some(query_list)) = use_window().match_media(query) {
return query_list.matches();
}
diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs
index 3bffcb5..52e829d 100644
--- a/src/use_color_mode.rs
+++ b/src/use_color_mode.rs
@@ -1,13 +1,10 @@
use crate::core::{ElementMaybeSignal, MaybeRwSignal};
-#[cfg(feature = "storage")]
-use crate::storage::{use_storage_with_options, UseStorageOptions};
-#[cfg(feature = "storage")]
-use serde::{Deserialize, Serialize};
+use crate::storage::{use_storage_with_options, StringCodec, UseStorageOptions};
use std::fmt::{Display, Formatter};
+use std::str::FromStr;
use crate::core::StorageType;
use crate::use_preferred_dark;
-use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder;
use leptos::*;
use std::marker::PhantomData;
@@ -16,9 +13,6 @@ use wasm_bindgen::JsCast;
/// Reactive color mode (dark / light / customs) with auto data persistence.
///
-/// > Data persistence is only enabled when the crate feature **`storage`** is enabled. You
-/// can use the function without it but the mode won't be persisted.
-///
/// ## Demo
///
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_color_mode)
@@ -27,7 +21,7 @@ use wasm_bindgen::JsCast;
///
/// ```
/// # use leptos::*;
-/// use leptos_use::{use_color_mode, UseColorModeReturn};
+/// # use leptos_use::{use_color_mode, UseColorModeReturn};
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
@@ -50,7 +44,7 @@ use wasm_bindgen::JsCast;
///
/// ```
/// # use leptos::*;
-/// use leptos_use::{ColorMode, use_color_mode, UseColorModeReturn};
+/// # use leptos_use::{ColorMode, use_color_mode, UseColorModeReturn};
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
@@ -58,7 +52,7 @@ use wasm_bindgen::JsCast;
/// #
/// mode.get(); // ColorMode::Dark or ColorMode::Light
///
-/// set_mode.set(ColorMode::Dark); // change to dark mode and persist (with feature `storage`)
+/// set_mode.set(ColorMode::Dark); // change to dark mode and persist
///
/// set_mode.set(ColorMode::Auto); // change to auto mode
/// #
@@ -70,7 +64,7 @@ use wasm_bindgen::JsCast;
///
/// ```
/// # use leptos::*;
-/// use leptos_use::{use_color_mode_with_options, UseColorModeOptions, UseColorModeReturn};
+/// # use leptos_use::{use_color_mode_with_options, UseColorModeOptions, UseColorModeReturn};
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
@@ -160,7 +154,7 @@ where
}
});
- let target = (target).into();
+ let target = target.into();
let update_html_attrs = {
move |target: ElementMaybeSignal,
@@ -255,49 +249,30 @@ pub enum ColorMode {
Custom(String),
}
-cfg_if! { if #[cfg(feature = "storage")] {
- fn get_store_signal(
- initial_value: MaybeRwSignal,
- storage_signal: Option>,
- storage_key: &str,
- storage_enabled: bool,
- storage: StorageType,
- listen_to_storage_changes: bool,
- ) -> (Signal, WriteSignal) {
- if let Some(storage_signal) = storage_signal {
- let (store, set_store) = storage_signal.split();
- (store.into(), set_store)
- } else if storage_enabled {
- let (store, set_store, _) = use_storage_with_options(
- storage_key,
- initial_value,
- UseStorageOptions::default()
- .listen_to_storage_changes(listen_to_storage_changes)
- .storage_type(storage),
- );
-
- (store, set_store)
- } else {
- initial_value.into_signal()
- }
+fn get_store_signal(
+ initial_value: MaybeRwSignal,
+ storage_signal: Option>,
+ storage_key: &str,
+ storage_enabled: bool,
+ storage: StorageType,
+ listen_to_storage_changes: bool,
+) -> (Signal, WriteSignal) {
+ if let Some(storage_signal) = storage_signal {
+ let (store, set_store) = storage_signal.split();
+ (store.into(), set_store)
+ } else if storage_enabled {
+ let (store, set_store, _) = use_storage_with_options::(
+ storage,
+ storage_key,
+ UseStorageOptions::default()
+ .listen_to_storage_changes(listen_to_storage_changes)
+ .initial_value(initial_value),
+ );
+ (store, set_store)
+ } else {
+ initial_value.into_signal()
}
-} else {
- fn get_store_signal(
- initial_value: MaybeRwSignal,
- storage_signal: Option>,
- _storage_key: &str,
- _storage_enabled: bool,
- _storage: StorageType,
- _listen_to_storage_changes: bool,
- ) -> (Signal, WriteSignal) {
- if let Some(storage_signal) = storage_signal {
- let (store, set_store) = storage_signal.split();
- (store.into(), set_store)
- } else {
- initial_value.into_signal()
- }
- }
-}}
+}
impl Display for ColorMode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
@@ -330,6 +305,14 @@ impl From for ColorMode {
}
}
+impl FromStr for ColorMode {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result {
+ Ok(ColorMode::from(s))
+ }
+}
+
#[derive(DefaultBuilder)]
pub struct UseColorModeOptions
where
@@ -378,7 +361,7 @@ where
/// If the color mode should be persisted. If `true` this required the
/// *create feature* **`storage`** to be enabled.
- /// Defaults to `true` and is forced to `false` if the feature **`storage`** is not enabled.
+ /// Defaults to `true`.
storage_enabled: bool,
/// Emit `auto` mode from state
diff --git a/src/use_css_var.rs b/src/use_css_var.rs
index 41f1d04..0b0a803 100644
--- a/src/use_css_var.rs
+++ b/src/use_css_var.rs
@@ -105,7 +105,7 @@ where
let (variable, set_variable) = create_signal(initial_value.clone());
cfg_if! { if #[cfg(not(feature = "ssr"))] {
- let el_signal = (target).into();
+ let el_signal = target.into();
let prop = prop.into();
let update_css_var = {
diff --git a/src/use_display_media.rs b/src/use_display_media.rs
new file mode 100644
index 0000000..fb0ff8c
--- /dev/null
+++ b/src/use_display_media.rs
@@ -0,0 +1,191 @@
+use crate::core::MaybeRwSignal;
+use cfg_if::cfg_if;
+use default_struct_builder::DefaultBuilder;
+use leptos::*;
+use wasm_bindgen::{JsCast, JsValue};
+
+/// Reactive [`mediaDevices.getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) streaming.
+///
+/// ## Demo
+///
+/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_display_media)
+///
+/// ## Usage
+///
+/// ```
+/// # use leptos::*;
+/// # use leptos_use::{use_display_media, UseDisplayMediaReturn};
+/// #
+/// # #[component]
+/// # fn Demo() -> impl IntoView {
+/// let video_ref = create_node_ref::();
+///
+/// let UseDisplayMediaReturn { stream, start, .. } = use_display_media();
+///
+/// start();
+///
+/// create_effect(move |_|
+/// video_ref.get().map(|v| {
+/// match stream.get() {
+/// Some(Ok(s)) => v.set_src_object(Some(&s)),
+/// Some(Err(e)) => logging::error!("Failed to get media stream: {:?}", e),
+/// None => logging::log!("No stream yet"),
+/// }
+/// })
+/// );
+///
+/// view! { }
+/// # }
+/// ```
+///
+/// ## Server-Side Rendering
+///
+/// On the server calls to `start` or any other way to enable the stream will be ignored
+/// and the stream will always be `None`.
+pub fn use_display_media() -> UseDisplayMediaReturn {
+ use_display_media_with_options(UseDisplayMediaOptions::default())
+}
+
+/// Version of [`use_display_media`] that accepts a [`UseDisplayMediaOptions`].
+pub fn use_display_media_with_options(
+ options: UseDisplayMediaOptions,
+) -> UseDisplayMediaReturn {
+ let UseDisplayMediaOptions { enabled, audio } = options;
+
+ let (enabled, set_enabled) = enabled.into_signal();
+
+ let (stream, set_stream) = create_signal(None::>);
+
+ let _start = move || async move {
+ cfg_if! { if #[cfg(not(feature = "ssr"))] {
+ if stream.get_untracked().is_some() {
+ return;
+ }
+
+ let stream = create_media(audio).await;
+
+ set_stream.update(|s| *s = Some(stream));
+ } else {
+ let _ = audio;
+ }}
+ };
+
+ let _stop = move || {
+ if let Some(Ok(stream)) = stream.get_untracked() {
+ for track in stream.get_tracks() {
+ track.unchecked_ref::().stop();
+ }
+ }
+
+ set_stream.set(None);
+ };
+
+ let start = move || {
+ cfg_if! { if #[cfg(not(feature = "ssr"))] {
+ spawn_local(async move {
+ _start().await;
+ stream.with_untracked(move |stream| {
+ if let Some(Ok(_)) = stream {
+ set_enabled.set(true);
+ }
+ });
+ });
+ }}
+ };
+
+ let stop = move || {
+ _stop();
+ set_enabled.set(false);
+ };
+
+ let _ = watch(
+ move || enabled.get(),
+ move |enabled, _, _| {
+ if *enabled {
+ spawn_local(async move {
+ _start().await;
+ });
+ } else {
+ _stop();
+ }
+ },
+ true,
+ );
+
+ UseDisplayMediaReturn {
+ stream: stream.into(),
+ start,
+ stop,
+ enabled,
+ set_enabled,
+ }
+}
+
+#[cfg(not(feature = "ssr"))]
+async fn create_media(audio: bool) -> Result {
+ use crate::use_window::use_window;
+
+ let media = use_window()
+ .navigator()
+ .ok_or_else(|| JsValue::from_str("Failed to access window.navigator"))
+ .and_then(|n| n.media_devices())?;
+
+ let mut constraints = web_sys::DisplayMediaStreamConstraints::new();
+ if audio {
+ constraints.audio(&JsValue::from(true));
+ }
+
+ let promise = media.get_display_media_with_constraints(&constraints)?;
+ let res = wasm_bindgen_futures::JsFuture::from(promise).await?;
+
+ Ok::<_, JsValue>(web_sys::MediaStream::unchecked_from_js(res))
+}
+
+// NOTE: there's no video value because it has to be `true`. Otherwise the stream would always resolve to an Error.
+/// Options for [`use_display_media`].
+#[derive(DefaultBuilder, Clone, Copy, Debug)]
+pub struct UseDisplayMediaOptions {
+ /// If the stream is enabled. Defaults to `false`.
+ enabled: MaybeRwSignal,
+
+ /// A value of `true` indicates that the returned [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream)
+ /// will contain an audio track, if audio is supported and available for the display surface chosen by the user.
+ /// The default value is `false`.
+ audio: bool,
+}
+
+impl Default for UseDisplayMediaOptions {
+ fn default() -> Self {
+ Self {
+ enabled: false.into(),
+ audio: false,
+ }
+ }
+}
+
+/// Return type of [`use_display_media`]
+#[derive(Clone)]
+pub struct UseDisplayMediaReturn
+where
+ StartFn: Fn() + Clone,
+ StopFn: Fn() + Clone,
+{
+ /// The current [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) if it exists.
+ /// Initially this is `None` until `start` resolved successfully.
+ /// In case the stream couldn't be started, for example because the user didn't grant permission,
+ /// this has the value `Some(Err(...))`.
+ pub stream: Signal>>,
+
+ /// Starts the screen streaming. Triggers the ask for permission if not already granted.
+ pub start: StartFn,
+
+ /// Stops the screen streaming
+ pub stop: StopFn,
+
+ /// A value of `true` indicates that the returned [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream)
+ /// has resolved successfully and thus the stream is enabled.
+ pub enabled: Signal,
+
+ /// A value of `true` is the same as calling `start()` whereas `false` is the same as calling `stop()`.
+ pub set_enabled: WriteSignal,
+}
diff --git a/src/use_document.rs b/src/use_document.rs
index 2a7a1bb..d9afaac 100644
--- a/src/use_document.rs
+++ b/src/use_document.rs
@@ -1,6 +1,7 @@
use cfg_if::cfg_if;
use std::ops::Deref;
+use crate::core::impl_ssr_safe_method;
#[cfg(not(feature = "ssr"))]
use leptos::*;
@@ -46,11 +47,15 @@ impl Deref for UseDocument {
}
impl UseDocument {
- pub fn body(&self) -> Option {
- self.0.as_ref().and_then(|d| d.body())
- }
+ impl_ssr_safe_method!(
+ /// Returns `Some(Document)` in the Browser. `None` otherwise.
+ body(&self) -> Option;
+ .unwrap_or_default()
+ );
- pub fn active_element(&self) -> Option {
- self.0.as_ref().and_then(|d| d.active_element())
- }
+ impl_ssr_safe_method!(
+ /// Returns the active (focused) `Some(web_sys::Element)` in the Browser. `None` otherwise.
+ active_element(&self) -> Option;
+ .unwrap_or_default()
+ );
}
diff --git a/src/use_draggable.rs b/src/use_draggable.rs
index b293626..c44aef8 100644
--- a/src/use_draggable.rs
+++ b/src/use_draggable.rs
@@ -1,5 +1,5 @@
use crate::core::{ElementMaybeSignal, MaybeRwSignal, PointerType, Position};
-use crate::{use_event_listener_with_options, UseEventListenerOptions};
+use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions, UseWindow};
use default_struct_builder::DefaultBuilder;
use leptos::ev::{pointerdown, pointermove, pointerup};
use leptos::*;
@@ -52,8 +52,8 @@ where
use_draggable_with_options::<
El,
T,
- web_sys::EventTarget,
- web_sys::EventTarget,
+ UseWindow,
+ web_sys::Window,
web_sys::EventTarget,
web_sys::EventTarget,
>(target, UseDraggableOptions::default())
@@ -87,7 +87,7 @@ where
..
} = options;
- let target = (target).into();
+ let target = target.into();
let dragging_handle = if let Some(handle) = handle {
let handle = (handle).into();
@@ -267,19 +267,14 @@ where
}
impl Default
- for UseDraggableOptions<
- web_sys::EventTarget,
- web_sys::EventTarget,
- web_sys::EventTarget,
- web_sys::EventTarget,
- >
+ for UseDraggableOptions
{
fn default() -> Self {
Self {
exact: MaybeSignal::default(),
prevent_default: MaybeSignal::default(),
stop_propagation: MaybeSignal::default(),
- dragging_element: window().into(),
+ dragging_element: use_window(),
handle: None,
pointer_types: vec![PointerType::Mouse, PointerType::Touch, PointerType::Pen],
initial_value: MaybeRwSignal::default(),
diff --git a/src/use_element_size.rs b/src/use_element_size.rs
index 8831505..bc6a226 100644
--- a/src/use_element_size.rs
+++ b/src/use_element_size.rs
@@ -12,9 +12,6 @@ cfg_if! { if #[cfg(not(feature = "ssr"))] {
/// Reactive size of an HTML element.
///
-/// > This function requires `--cfg=web_sys_unstable_apis` to be activated as
-/// [described in the wasm-bindgen guide](https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html).
-///
/// Please refer to [ResizeObserver on MDN](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
/// for more details.
///
@@ -25,12 +22,12 @@ cfg_if! { if #[cfg(not(feature = "ssr"))] {
/// ## Usage
///
/// ```
-/// # use leptos::*;
+/// # use leptos::{html::Div, *};
/// # use leptos_use::{use_element_size, UseElementSizeReturn};
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
-/// let el = create_node_ref();
+/// let el = create_node_ref::();
///
/// let UseElementSizeReturn { width, height } = use_element_size(el);
///
@@ -175,7 +172,7 @@ where
}
}
-#[derive(DefaultBuilder)]
+#[derive(DefaultBuilder, Default)]
/// Options for [`use_element_size_with_options`].
pub struct UseElementSizeOptions {
/// Initial size returned before any measurements on the `target` are done. Also the value reported
@@ -187,15 +184,6 @@ pub struct UseElementSizeOptions {
pub box_: Option
,
}
-impl Default for UseElementSizeOptions {
- fn default() -> Self {
- Self {
- initial_size: Size::default(),
- box_: None,
- }
- }
-}
-
/// The return value of [`use_element_size`].
pub struct UseElementSizeReturn {
/// The width of the element.
diff --git a/src/use_event_listener.rs b/src/use_event_listener.rs
index e8c218e..ec7c2a8 100644
--- a/src/use_event_listener.rs
+++ b/src/use_event_listener.rs
@@ -132,7 +132,7 @@ where
let event_name = event.name();
- let signal = (target).into();
+ let signal = target.into();
let prev_element = Rc::new(RefCell::new(None::));
diff --git a/src/use_idle.rs b/src/use_idle.rs
index 247c867..3f0a20e 100644
--- a/src/use_idle.rs
+++ b/src/use_idle.rs
@@ -1,14 +1,9 @@
-use crate::utils::{create_filter_wrapper, DebounceOptions, FilterOptions, ThrottleOptions};
-use crate::{
- filter_builder_methods, use_document, use_event_listener, use_event_listener_with_options,
- UseEventListenerOptions,
-};
+use crate::core::now;
+use crate::filter_builder_methods;
+use crate::utils::{DebounceOptions, FilterOptions, ThrottleOptions};
+use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder;
-use leptos::ev::{visibilitychange, Custom};
-use leptos::leptos_dom::helpers::TimeoutHandle;
use leptos::*;
-use std::cell::Cell;
-use std::time::Duration;
///
///
@@ -54,6 +49,18 @@ use std::time::Duration;
/// # view! { }
/// # }
/// ```
+///
+/// ## Server-Side Rendering
+///
+/// On the server this will always return static signals
+///
+/// ```ignore
+/// UseIdleReturn{
+/// idle: Signal(initial_state),
+/// last_active: Signal(now),
+/// reset: || {}
+/// }
+/// ```
pub fn use_idle(timeout: u64) -> UseIdleReturn {
use_idle_with_options(timeout, UseIdleOptions::default())
}
@@ -71,57 +78,78 @@ pub fn use_idle_with_options(
} = options;
let (idle, set_idle) = create_signal(initial_state);
- let (last_active, set_last_active) = create_signal(js_sys::Date::now());
+ let (last_active, set_last_active) = create_signal(now());
- let reset = {
- let timer = Cell::new(None::);
+ cfg_if! { if #[cfg(feature = "ssr")] {
+ let reset = || ();
+ let _ = timeout;
+ let _ = events;
+ let _ = listen_for_visibility_change;
+ let _ = filter;
+ let _ = set_last_active;
+ let _ = set_idle;
+ } else {
+ use crate::utils::create_filter_wrapper;
+ use crate::{
+ use_document, use_event_listener, use_event_listener_with_options, UseEventListenerOptions,
+ };
+ use leptos::ev::{visibilitychange, Custom};
+ use leptos::leptos_dom::helpers::TimeoutHandle;
+ use std::cell::Cell;
+ use std::rc::Rc;
+ use std::time::Duration;
- move || {
- set_idle.set(false);
- if let Some(timer) = timer.take() {
- timer.clear();
+ let timer = Rc::new(Cell::new(None::));
+
+ let reset = {
+ let timer = Rc::clone(&timer);
+
+ move || {
+ set_idle.set(false);
+ if let Some(timer) = timer.replace(
+ set_timeout_with_handle(move || set_idle.set(true), Duration::from_millis(timeout))
+ .ok(),
+ ) {
+ timer.clear();
+ }
}
- timer.replace(
- set_timeout_with_handle(move || set_idle.set(true), Duration::from_millis(timeout))
- .ok(),
+ };
+
+ let on_event = {
+ let reset = reset.clone();
+
+ let filtered_callback = create_filter_wrapper(filter.filter_fn(), move || {
+ set_last_active.set(js_sys::Date::now());
+ reset();
+ });
+
+ move |_: web_sys::Event| {
+ filtered_callback();
+ }
+ };
+
+ let listener_options = UseEventListenerOptions::default().passive(true);
+ for event in events {
+ let _ = use_event_listener_with_options(
+ use_document(),
+ Custom::new(event),
+ on_event.clone(),
+ listener_options,
);
}
- };
- let on_event = {
- let reset = reset.clone();
+ if listen_for_visibility_change {
+ let on_event = on_event.clone();
- let filtered_callback = create_filter_wrapper(filter.filter_fn(), move || {
- set_last_active.set(js_sys::Date::now());
- reset();
- });
-
- move |_: web_sys::Event| {
- filtered_callback();
+ let _ = use_event_listener(use_document(), visibilitychange, move |evt| {
+ if !document().hidden() {
+ on_event(evt);
+ }
+ });
}
- };
- let listener_options = UseEventListenerOptions::default().passive(true);
- for event in events {
- let _ = use_event_listener_with_options(
- use_document(),
- Custom::new(event),
- on_event.clone(),
- listener_options,
- );
- }
-
- if listen_for_visibility_change {
- let on_event = on_event.clone();
-
- let _ = use_event_listener(use_document(), visibilitychange, move |evt| {
- if !document().hidden() {
- on_event(evt);
- }
- });
- }
-
- reset.clone()();
+ reset.clone()();
+ }}
UseIdleReturn {
idle: idle.into(),
diff --git a/src/use_infinite_scroll.rs b/src/use_infinite_scroll.rs
new file mode 100644
index 0000000..2b23261
--- /dev/null
+++ b/src/use_infinite_scroll.rs
@@ -0,0 +1,255 @@
+use crate::core::{Direction, Directions, ElementMaybeSignal};
+use crate::{
+ use_element_visibility, use_scroll_with_options, ScrollOffset, UseEventListenerOptions,
+ UseScrollOptions, UseScrollReturn,
+};
+use default_struct_builder::DefaultBuilder;
+use futures_util::join;
+use gloo_timers::future::sleep;
+use leptos::*;
+use std::future::Future;
+use std::rc::Rc;
+use std::time::Duration;
+use wasm_bindgen::JsCast;
+
+/// Infinite scrolling of the element.
+///
+/// ## Demo
+///
+/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_infinite_scroll)
+///
+/// ## Usage
+///
+/// ```
+/// # use leptos::*;
+/// use leptos::html::Div;
+/// # use leptos_use::{use_infinite_scroll_with_options, UseInfiniteScrollOptions};
+/// #
+/// # #[component]
+/// # fn Demo() -> impl IntoView {
+/// let el = create_node_ref::();
+///
+/// let (data, set_data) = create_signal(vec![1, 2, 3, 4, 5, 6]);
+///
+/// let _ = use_infinite_scroll_with_options(
+/// el,
+/// move |_| async move {
+/// let len = data.with(|d| d.len());
+/// set_data.update(|data| *data = (1..len+6).collect());
+/// },
+/// UseInfiniteScrollOptions::default().distance(10.0),
+/// );
+///
+/// view! {
+///
+/// { item }
+///
+/// }
+/// # }
+/// ```
+///
+/// The returned signal is `true` while new data is being loaded.
+pub fn use_infinite_scroll
(el: El, on_load_more: LFn) -> Signal
+where
+ El: Into> + Clone + 'static,
+ T: Into + Clone + 'static,
+ LFn: Fn(ScrollState) -> LFut + 'static,
+ LFut: Future,
+{
+ use_infinite_scroll_with_options(el, on_load_more, UseInfiniteScrollOptions::default())
+}
+
+/// Version of [`use_infinite_scroll`] that takes a `UseInfiniteScrollOptions`. See [`use_infinite_scroll`] for how to use.
+pub fn use_infinite_scroll_with_options(
+ el: El,
+ on_load_more: LFn,
+ options: UseInfiniteScrollOptions,
+) -> Signal
+where
+ El: Into> + Clone + 'static,
+ T: Into + Clone + 'static,
+ LFn: Fn(ScrollState) -> LFut + 'static,
+ LFut: Future,
+{
+ let UseInfiniteScrollOptions {
+ distance,
+ direction,
+ interval,
+ on_scroll,
+ event_listener_options,
+ } = options;
+
+ let on_load_more = store_value(on_load_more);
+
+ let UseScrollReturn {
+ x,
+ y,
+ is_scrolling,
+ arrived_state,
+ directions,
+ measure,
+ ..
+ } = use_scroll_with_options(
+ el.clone(),
+ UseScrollOptions::default()
+ .on_scroll(move |evt| on_scroll(evt))
+ .event_listener_options(event_listener_options)
+ .offset(ScrollOffset::default().set_direction(direction, distance)),
+ );
+
+ let state = ScrollState {
+ x,
+ y,
+ is_scrolling,
+ arrived_state,
+ directions,
+ };
+
+ let (is_loading, set_loading) = create_signal(false);
+
+ let el = el.into();
+ let observed_element = create_memo(move |_| {
+ let el = el.get();
+
+ el.map(|el| {
+ let el = el.into();
+
+ if el.is_instance_of::() || el.is_instance_of::() {
+ document()
+ .document_element()
+ .expect("document element not found")
+ } else {
+ el
+ }
+ })
+ });
+
+ let is_element_visible = use_element_visibility(observed_element);
+
+ let check_and_load = store_value(None::>);
+
+ check_and_load.set_value(Some(Rc::new({
+ let measure = measure.clone();
+
+ move || {
+ let observed_element = observed_element.get_untracked();
+
+ if !is_element_visible.get_untracked() {
+ return;
+ }
+
+ if let Some(observed_element) = observed_element {
+ let scroll_height = observed_element.scroll_height();
+ let client_height = observed_element.client_height();
+ let scroll_width = observed_element.scroll_width();
+ let client_width = observed_element.client_width();
+
+ let is_narrower = if direction == Direction::Bottom || direction == Direction::Top {
+ scroll_height <= client_height
+ } else {
+ scroll_width <= client_width
+ };
+
+ if (state.arrived_state.get_untracked().get_direction(direction) || is_narrower)
+ && !is_loading.get_untracked()
+ {
+ set_loading.set(true);
+
+ let measure = measure.clone();
+ spawn_local(async move {
+ join!(
+ on_load_more.with_value(|f| f(state)),
+ sleep(Duration::from_millis(interval as u64))
+ );
+
+ set_loading.set(false);
+ sleep(Duration::ZERO).await;
+ measure();
+ if let Some(check_and_load) = check_and_load.get_value() {
+ check_and_load();
+ }
+ });
+ }
+ }
+ }
+ })));
+
+ let _ = watch(
+ move || is_element_visible.get(),
+ move |visible, prev_visible, _| {
+ if *visible && !prev_visible.copied().unwrap_or_default() {
+ measure();
+ }
+ },
+ true,
+ );
+
+ let _ = watch(
+ move || state.arrived_state.get().get_direction(direction),
+ move |arrived, prev_arrived, _| {
+ if let Some(prev_arrived) = prev_arrived {
+ if prev_arrived == arrived {
+ return;
+ }
+ }
+
+ check_and_load
+ .get_value()
+ .expect("check_and_load is set above")()
+ },
+ true,
+ );
+
+ is_loading.into()
+}
+
+/// Options for [`use_infinite_scroll_with_options`].
+#[derive(DefaultBuilder)]
+pub struct UseInfiniteScrollOptions {
+ /// Callback when scrolling is happening.
+ on_scroll: Rc,
+
+ /// Options passed to the `addEventListener("scroll", ...)` call
+ event_listener_options: UseEventListenerOptions,
+
+ /// The minimum distance between the bottom of the element and the bottom of the viewport. Default is 0.0.
+ distance: f64,
+
+ /// The direction in which to listen the scroll. Defaults to `Direction::Bottom`.
+ direction: Direction,
+
+ /// The interval time between two load more (to avoid too many invokes). Default is 100.0.
+ interval: f64,
+}
+
+impl Default for UseInfiniteScrollOptions {
+ fn default() -> Self {
+ Self {
+ on_scroll: Rc::new(|_| {}),
+ event_listener_options: Default::default(),
+ distance: 0.0,
+ direction: Direction::Bottom,
+ interval: 100.0,
+ }
+ }
+}
+
+/// The scroll state being passed into the `on_load_more` callback of [`use_infinite_scroll`].
+#[derive(Copy, Clone)]
+pub struct ScrollState {
+ /// X coordinate of scroll position
+ pub x: Signal,
+
+ /// Y coordinate of scroll position
+ pub y: Signal,
+
+ /// Is true while the element is being scrolled.
+ pub is_scrolling: Signal,
+
+ /// Sets the field that represents a direction to true if the
+ /// element is scrolled all the way to that side.
+ pub arrived_state: Signal,
+
+ /// The directions in which the element is being scrolled are set to true.
+ pub directions: Signal,
+}
diff --git a/src/use_intersection_observer.rs b/src/use_intersection_observer.rs
index fda59bf..78c43fb 100644
--- a/src/use_intersection_observer.rs
+++ b/src/use_intersection_observer.rs
@@ -124,7 +124,7 @@ where
}
};
- let targets = (target).into();
+ let targets = target.into();
let root = root.map(|root| (root).into());
let stop_watch = {
diff --git a/src/use_mouse.rs b/src/use_mouse.rs
index 5e4e17b..f81741a 100644
--- a/src/use_mouse.rs
+++ b/src/use_mouse.rs
@@ -1,7 +1,7 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::core::{ElementMaybeSignal, Position};
-use crate::{use_event_listener_with_options, UseEventListenerOptions};
+use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions, UseWindow};
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder;
use leptos::ev::{dragover, mousemove, touchend, touchmove, touchstart};
@@ -208,8 +208,7 @@ where
/// Options for [`use_mouse_with_options`].
pub struct UseMouseOptions
where
- El: Clone,
- El: Into>,
+ El: Clone + Into>,
T: Into + Clone + 'static,
Ex: UseMouseEventExtractor + Clone,
{
@@ -232,33 +231,18 @@ where
_marker: PhantomData,
}
-cfg_if! { if #[cfg(feature = "ssr")] {
- impl Default for UseMouseOptions, web_sys::Window, UseMouseEventExtractorDefault> {
- fn default() -> Self {
- Self {
- coord_type: UseMouseCoordType::::default(),
- target: None,
- touch: true,
- reset_on_touch_ends: false,
- initial_value: Position { x: 0.0, y: 0.0 },
- _marker: Default::default(),
- }
+impl Default for UseMouseOptions {
+ fn default() -> Self {
+ Self {
+ coord_type: UseMouseCoordType::::default(),
+ target: use_window(),
+ touch: true,
+ reset_on_touch_ends: false,
+ initial_value: Position { x: 0.0, y: 0.0 },
+ _marker: PhantomData,
}
}
-} else {
- impl Default for UseMouseOptions {
- fn default() -> Self {
- Self {
- coord_type: UseMouseCoordType::::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 f518000..91f185a 100644
--- a/src/use_mutation_observer.rs
+++ b/src/use_mutation_observer.rs
@@ -109,7 +109,7 @@ where
}
};
- let targets = (target).into();
+ let targets = target.into();
let stop_watch = {
let cleanup = cleanup.clone();
diff --git a/src/use_raf_fn.rs b/src/use_raf_fn.rs
index c44a00a..27c7389 100644
--- a/src/use_raf_fn.rs
+++ b/src/use_raf_fn.rs
@@ -1,10 +1,9 @@
use crate::utils::Pausable;
+use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder;
use leptos::*;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
-use wasm_bindgen::closure::Closure;
-use wasm_bindgen::JsCast;
/// Call function on every requestAnimationFrame.
/// With controls of pausing and resuming.
@@ -34,6 +33,10 @@ use wasm_bindgen::JsCast;
///
/// You can use `use_raf_fn_with_options` and set `immediate` to `false`. In that case
/// you have to call `resume()` before the `callback` is executed.
+///
+/// ## Server-Side Rendering
+///
+/// On the server this does basically nothing. The provided closure will never be called.
pub fn use_raf_fn(
callback: impl Fn(UseRafFnCallbackArgs) + 'static,
) -> Pausable {
@@ -54,24 +57,31 @@ pub fn use_raf_fn_with_options(
let loop_ref = Rc::new(RefCell::new(Box::new(|_: f64| {}) as Box));
let request_next_frame = {
- let loop_ref = Rc::clone(&loop_ref);
- let raf_handle = Rc::clone(&raf_handle);
+ cfg_if! { if #[cfg(feature = "ssr")] {
+ move || ()
+ } else {
+ use wasm_bindgen::JsCast;
+ use wasm_bindgen::closure::Closure;
- move || {
let loop_ref = Rc::clone(&loop_ref);
+ let raf_handle = Rc::clone(&raf_handle);
- raf_handle.set(
- window()
- .request_animation_frame(
- Closure::once_into_js(move |timestamp: f64| {
- loop_ref.borrow()(timestamp);
- })
- .as_ref()
- .unchecked_ref(),
- )
- .ok(),
- );
- }
+ move || {
+ let loop_ref = Rc::clone(&loop_ref);
+
+ raf_handle.set(
+ window()
+ .request_animation_frame(
+ Closure::once_into_js(move |timestamp: f64| {
+ loop_ref.borrow()(timestamp);
+ })
+ .as_ref()
+ .unchecked_ref(),
+ )
+ .ok(),
+ );
+ }
+ }}
};
let loop_fn = {
@@ -79,7 +89,7 @@ pub fn use_raf_fn_with_options(
let previous_frame_timestamp = Cell::new(0.0_f64);
move |timestamp: f64| {
- if !is_active.get() {
+ if !is_active.get_untracked() {
return;
}
@@ -101,7 +111,7 @@ pub fn use_raf_fn_with_options(
let _ = loop_ref.replace(Box::new(loop_fn));
let resume = move || {
- if !is_active.get() {
+ if !is_active.get_untracked() {
set_active.set(true);
request_next_frame();
}
diff --git a/src/use_resize_observer.rs b/src/use_resize_observer.rs
index 1c61081..07e5dd4 100644
--- a/src/use_resize_observer.rs
+++ b/src/use_resize_observer.rs
@@ -12,9 +12,6 @@ cfg_if! { if #[cfg(not(feature = "ssr"))] {
/// Reports changes to the dimensions of an Element's content or the border-box.
///
-/// > This function requires `--cfg=web_sys_unstable_apis` to be activated as
-/// [described in the wasm-bindgen guide](https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html).
-///
/// Please refer to [ResizeObserver on MDN](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
/// for more details.
///
@@ -25,12 +22,12 @@ cfg_if! { if #[cfg(not(feature = "ssr"))] {
/// ## Usage
///
/// ```
-/// # use leptos::*;
+/// # use leptos::{html::Div, *};
/// # use leptos_use::use_resize_observer;
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
-/// let el = create_node_ref();
+/// let el = create_node_ref::();
/// let (text, set_text) = create_signal("".to_string());
///
/// use_resize_observer(
@@ -114,7 +111,7 @@ where
}
};
- let targets = (target).into();
+ let targets = target.into();
let stop_watch = {
let cleanup = cleanup.clone();
diff --git a/src/use_scroll.rs b/src/use_scroll.rs
index 94c2c12..757c6dc 100644
--- a/src/use_scroll.rs
+++ b/src/use_scroll.rs
@@ -1,4 +1,4 @@
-use crate::core::ElementMaybeSignal;
+use crate::core::{Direction, Directions, ElementMaybeSignal};
use crate::UseEventListenerOptions;
use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder;
@@ -174,7 +174,9 @@ const ARRIVED_STATE_THRESHOLD_PIXELS: f64 = 1.0;
/// ## Server-Side Rendering
///
/// On the server this returns signals that don't change and setters that are noops.
-pub fn use_scroll(element: El) -> UseScrollReturn
+pub fn use_scroll(
+ element: El,
+) -> UseScrollReturn
where
El: Clone,
El: Into>,
@@ -185,7 +187,10 @@ where
/// Version of [`use_scroll`] with options. See [`use_scroll`] for how to use.
#[cfg_attr(feature = "ssr", allow(unused_variables))]
-pub fn use_scroll_with_options(element: El, options: UseScrollOptions) -> UseScrollReturn
+pub fn use_scroll_with_options(
+ element: El,
+ options: UseScrollOptions,
+) -> UseScrollReturn
where
El: Clone,
El: Into>,
@@ -210,9 +215,9 @@ where
});
cfg_if! { if #[cfg(feature = "ssr")] {
- let set_x = Box::new(|_| {});
- let set_y = Box::new(|_| {});
- let measure = Box::new(|| {});
+ let set_x = |_| {};
+ let set_y = |_| {};
+ let measure = || {};
} else {
let signal = element.into();
let behavior = options.behavior;
@@ -243,16 +248,16 @@ where
let set_x = {
let scroll_to = scroll_to.clone();
- Box::new(move |x| scroll_to(Some(x), None))
+ move |x| scroll_to(Some(x), None)
};
- let set_y = Box::new(move |y| scroll_to(None, Some(y)));
+ let set_y = move |y| scroll_to(None, Some(y));
let on_scroll_end = {
let on_stop = Rc::clone(&options.on_stop);
move |e| {
- if !is_scrolling.get_untracked() {
+ if !is_scrolling.try_get_untracked().unwrap_or_default() {
return;
}
@@ -385,12 +390,7 @@ where
Signal>,
web_sys::EventTarget,
_,
- >(
- target,
- ev::scroll,
- handler,
- options.event_listener_options,
- );
+ >(target, ev::scroll, handler, options.event_listener_options);
} else {
let _ = use_event_listener_with_options::<
_,
@@ -417,13 +417,12 @@ where
options.event_listener_options,
);
- let measure = Box::new(move || {
- let el = signal.get_untracked();
- if let Some(el) = el {
+ let measure = move || {
+ if let Some(el) = signal.get_untracked() {
let el = el.into();
set_arrived_state(el);
}
- });
+ };
}}
UseScrollReturn {
@@ -501,26 +500,39 @@ impl From for web_sys::ScrollBehavior {
}
/// The return value of [`use_scroll`].
-pub struct UseScrollReturn {
+pub struct UseScrollReturn
+where
+ SetXFn: Fn(f64) + Clone,
+ SetYFn: Fn(f64) + Clone,
+ MFn: Fn() + Clone,
+{
+ /// X coordinate of scroll position
pub x: Signal,
- pub set_x: Box,
+
+ /// Sets the value of `x`. This does also scroll the element.
+ pub set_x: SetXFn,
+
+ /// Y coordinate of scroll position
pub y: Signal