From 550a8004d8def020933a2f842d040a66e7cbf0e0 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Sat, 30 Sep 2023 18:45:36 +0100 Subject: [PATCH 01/90] fixed book demos build --- docs/book/post_build.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/book/post_build.py b/docs/book/post_build.py index 4185033..1abc3a2 100644 --- a/docs/book/post_build.py +++ b/docs/book/post_build.py @@ -2,6 +2,7 @@ import os import shutil import subprocess import sys +import re def main(): @@ -43,7 +44,7 @@ def build_and_copy_demo(category, md_name): html = f.read() head_split = html.split("") target_head = head_split[1].split("")[0] - body_split = html.split("")[1].split("") + body_split = re.split("]*>", html)[1].split("") target_body = body_split[0] with open(book_html_path, "w") as f: From 50345a2befad2f6fdb10febc3f48f931023c3e9c Mon Sep 17 00:00:00 2001 From: Maccesch Date: Mon, 2 Oct 2023 21:05:20 +0100 Subject: [PATCH 02/90] added use_sorted --- .idea/leptos-use.iml | 1 + CHANGELOG.md | 6 + Cargo.toml | 2 +- docs/book/src/SUMMARY.md | 5 +- docs/book/src/iterable/use_sorted.md | 3 + examples/Cargo.toml | 1 + examples/use_sorted/Cargo.toml | 16 + examples/use_sorted/README.md | 23 ++ examples/use_sorted/Trunk.toml | 2 + examples/use_sorted/index.html | 7 + examples/use_sorted/input.css | 3 + examples/use_sorted/rust-toolchain.toml | 2 + examples/use_sorted/src/main.rs | 44 +++ examples/use_sorted/style/output.css | 289 ++++++++++++++++++ examples/use_sorted/tailwind.config.js | 15 + .../server/cert.crt | 24 ++ .../server/cert.key | 28 ++ src/lib.rs | 2 + src/use_sorted.rs | 125 ++++++++ 19 files changed, 596 insertions(+), 2 deletions(-) create mode 100644 docs/book/src/iterable/use_sorted.md create mode 100644 examples/use_sorted/Cargo.toml create mode 100644 examples/use_sorted/README.md create mode 100644 examples/use_sorted/Trunk.toml create mode 100644 examples/use_sorted/index.html create mode 100644 examples/use_sorted/input.css create mode 100644 examples/use_sorted/rust-toolchain.toml create mode 100644 examples/use_sorted/src/main.rs create mode 100644 examples/use_sorted/style/output.css create mode 100644 examples/use_sorted/tailwind.config.js create mode 100644 examples/use_webtransport_with_server/server/cert.crt create mode 100644 examples/use_webtransport_with_server/server/cert.key create mode 100644 src/use_sorted.rs diff --git a/.idea/leptos-use.iml b/.idea/leptos-use.iml index ad25f2d..df1b62f 100644 --- a/.idea/leptos-use.iml +++ b/.idea/leptos-use.iml @@ -56,6 +56,7 @@ + diff --git a/CHANGELOG.md b/CHANGELOG.md index c49cdcf..65d75ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.1] - 2023-10-02 + +### New Function 🚀 + +- `use_sorted` + ## [0.7.0] - 2023-09-30 ### New Functions 🚀 diff --git a/Cargo.toml b/Cargo.toml index 23ca4bb..2747c15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos-use" -version = "0.7.0" +version = "0.7.1" edition = "2021" authors = ["Marc-Stefan Cassola"] categories = ["gui", "web-programming"] diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index cd0e45c..dbb23c8 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -72,8 +72,11 @@ - [signal_debounced](reactivity/signal_debounced.md) - [signal_throttled](reactivity/signal_throttled.md) -# Utilities +# Iterable +- [use_sorted](iterable/use_sorted.md) + +# Utilities - [is_err](utilities/is_err.md) - [is_none](utilities/is_none.md) - [is_ok](utilities/is_ok.md) diff --git a/docs/book/src/iterable/use_sorted.md b/docs/book/src/iterable/use_sorted.md new file mode 100644 index 0000000..ad6b740 --- /dev/null +++ b/docs/book/src/iterable/use_sorted.md @@ -0,0 +1,3 @@ +# use_sorted + + diff --git a/examples/Cargo.toml b/examples/Cargo.toml index f7c5a5d..930d976 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -35,6 +35,7 @@ members = [ "use_resize_observer", "use_round", "use_scroll", + "use_sorted", "use_storage", "use_throttle_fn", "use_timestamp", 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_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/src/lib.rs b/src/lib.rs index 33a82f4..c3384c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,7 @@ mod use_preferred_contrast; mod use_preferred_dark; mod use_raf_fn; mod use_scroll; +mod use_sorted; mod use_supported; mod use_throttle_fn; mod use_timestamp; @@ -102,6 +103,7 @@ pub use use_preferred_contrast::*; pub use use_preferred_dark::*; pub use use_raf_fn::*; pub use use_scroll::*; +pub use use_sorted::*; pub use use_supported::*; pub use use_throttle_fn::*; pub use use_timestamp::*; diff --git a/src/use_sorted.rs b/src/use_sorted.rs new file mode 100644 index 0000000..32ff806 --- /dev/null +++ b/src/use_sorted.rs @@ -0,0 +1,125 @@ +use leptos::*; +use std::cmp::Ordering; +use std::ops::DerefMut; + +/// Reactive sort of iterable +/// +/// ## Demo +/// +/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_sorted) +/// +/// ## Usage +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::use_sorted; +/// # +/// # #[component] +/// # fn Demo() -> impl IntoView { +/// let source = vec![10, 3, 5, 7, 2, 1, 8, 6, 9, 4]; +/// let sorted = use_sorted(source); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +/// # +/// # view! { } +/// # } +/// ``` +/// +/// You can also sort by key or with a compare function. +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::{use_sorted_by, use_sorted_by_key}; +/// # +/// #[derive(Clone, PartialEq)] +/// pub struct Person { +/// pub name: String, +/// pub age: u16, +/// } +/// +/// # #[component] +/// # fn Demo() -> impl IntoView { +/// let source = vec![ +/// Person { +/// name: "John".to_string(), +/// age: 40, +/// }, +/// Person { +/// name: "Jane".to_string(), +/// age: 20, +/// }, +/// Person { +/// name: "Joe".to_string(), +/// age: 30, +/// }, +/// Person { +/// name: "Jenny".to_string(), +/// age: 22, +/// }, +/// ]; +/// +/// // sort by key +/// let sorted = use_sorted_by_key( +/// source.clone(), +/// |person| person.age, +/// ); +/// +/// // sort with compare function +/// let sorted = use_sorted_by( +/// source, +/// |p1, p2| p1.age.cmp(&p2.age), +/// ); +/// # +/// # view! { } +/// # } +/// ``` +/// +/// Please note that these two ways of sorting are equivalent. +pub fn use_sorted(iterable: S) -> Signal +where + S: Into>, + T: Ord, + I: DerefMut + Clone + PartialEq, +{ + let iterable = iterable.into(); + + create_memo(move |_| { + let mut iterable = iterable.get(); + iterable.sort(); + iterable + }) + .into() +} + +/// Version of [`use_sorted`] with a compare function. +pub fn use_sorted_by(iterable: S, cmp_fn: F) -> Signal +where + S: Into>, + I: DerefMut + Clone + PartialEq, + F: FnMut(&T, &T) -> Ordering + Clone + 'static, +{ + let iterable = iterable.into(); + + create_memo(move |_| { + let mut iterable = iterable.get(); + iterable.sort_by(cmp_fn.clone()); + iterable + }) + .into() +} + +/// Version of [`use_sorted`] by key. +pub fn use_sorted_by_key(iterable: S, key_fn: F) -> Signal +where + S: Into>, + I: DerefMut + Clone + PartialEq, + K: Ord, + F: FnMut(&T) -> K + Clone + 'static, +{ + let iterable = iterable.into(); + + create_memo(move |_| { + let mut iterable = iterable.get(); + iterable.sort_by_key(key_fn.clone()); + iterable + }) + .into() +} From 97d250cfd5929de7217590fbf87e0bdc5856eae3 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Mon, 2 Oct 2023 21:06:51 +0100 Subject: [PATCH 03/90] updated function count --- README.md | 2 +- docs/book/src/introduction.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bbfed70..f398a8d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Crates.io SSR Docs & Demos - 58 Functions + 59 Functions


diff --git a/docs/book/src/introduction.md b/docs/book/src/introduction.md index 5824f3a..5381e52 100644 --- a/docs/book/src/introduction.md +++ b/docs/book/src/introduction.md @@ -12,6 +12,6 @@ Crates.io SSR Docs & Demos - 58 Functions + 59 Functions

\ No newline at end of file From e038f9664a403dfaa592ff44091076aa14dfc5ee Mon Sep 17 00:00:00 2001 From: Lukas Potthast Date: Wed, 4 Oct 2023 17:35:52 +0200 Subject: [PATCH 04/90] WIP: Add use_service_worker --- CHANGELOG.md | 6 + Cargo.toml | 5 + docs/book/src/SUMMARY.md | 1 + docs/book/src/browser/use_service_worker.md | 3 + examples/Cargo.toml | 1 + examples/use_service_worker/Cargo.toml | 16 + examples/use_service_worker/README.md | 23 ++ examples/use_service_worker/Trunk.toml | 7 + examples/use_service_worker/index.html | 10 + examples/use_service_worker/input.css | 3 + examples/use_service_worker/manifest.json | 54 ++++ examples/use_service_worker/post_build.sh | 56 ++++ .../res/icon/maskable_icon_x128.png | Bin 0 -> 4308 bytes .../res/icon/maskable_icon_x192.png | Bin 0 -> 6145 bytes .../res/icon/maskable_icon_x384.png | Bin 0 -> 14111 bytes .../res/icon/maskable_icon_x48.png | Bin 0 -> 1341 bytes .../res/icon/maskable_icon_x512.png | Bin 0 -> 22051 bytes .../res/icon/maskable_icon_x72.png | Bin 0 -> 2408 bytes .../res/icon/maskable_icon_x96.png | Bin 0 -> 2865 bytes examples/use_service_worker/res/icon/pwa.png | Bin 0 -> 50089 bytes examples/use_service_worker/res/icon/pwa.svg | 9 + .../use_service_worker/rust-toolchain.toml | 2 + examples/use_service_worker/service-worker.js | 82 +++++ examples/use_service_worker/src/main.rs | 61 ++++ examples/use_service_worker/style/output.css | 294 ++++++++++++++++++ .../use_service_worker/tailwind.config.js | 15 + src/lib.rs | 2 + src/use_service_worker.rs | 269 ++++++++++++++++ 28 files changed, 919 insertions(+) create mode 100644 docs/book/src/browser/use_service_worker.md create mode 100644 examples/use_service_worker/Cargo.toml create mode 100644 examples/use_service_worker/README.md create mode 100644 examples/use_service_worker/Trunk.toml create mode 100644 examples/use_service_worker/index.html create mode 100644 examples/use_service_worker/input.css create mode 100644 examples/use_service_worker/manifest.json create mode 100755 examples/use_service_worker/post_build.sh create mode 100644 examples/use_service_worker/res/icon/maskable_icon_x128.png create mode 100644 examples/use_service_worker/res/icon/maskable_icon_x192.png create mode 100644 examples/use_service_worker/res/icon/maskable_icon_x384.png create mode 100644 examples/use_service_worker/res/icon/maskable_icon_x48.png create mode 100644 examples/use_service_worker/res/icon/maskable_icon_x512.png create mode 100644 examples/use_service_worker/res/icon/maskable_icon_x72.png create mode 100644 examples/use_service_worker/res/icon/maskable_icon_x96.png create mode 100644 examples/use_service_worker/res/icon/pwa.png create mode 100644 examples/use_service_worker/res/icon/pwa.svg create mode 100644 examples/use_service_worker/rust-toolchain.toml create mode 100644 examples/use_service_worker/service-worker.js create mode 100644 examples/use_service_worker/src/main.rs create mode 100644 examples/use_service_worker/style/output.css create mode 100644 examples/use_service_worker/tailwind.config.js create mode 100644 src/use_service_worker.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 65d75ca..213701a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] - + +### New Functions 🚀 + +- `use_service_worker` + ## [0.7.1] - 2023-10-02 ### New Function 🚀 diff --git a/Cargo.toml b/Cargo.toml index 2747c15..0031e6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ homepage = "https://leptos-use.rs" [dependencies] leptos = "0.5" wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" js-sys = "0.3" default-struct-builder = "0.5" num = { version = "0.4", optional = true } @@ -69,6 +70,10 @@ features = [ "ResizeObserverSize", "ScrollBehavior", "ScrollToOptions", + "ServiceWorker", + "ServiceWorkerContainer", + "ServiceWorkerRegistration", + "ServiceWorkerState", "Storage", "Touch", "TouchEvent", diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index dbb23c8..955db99 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -38,6 +38,7 @@ - [use_media_query](browser/use_media_query.md) - [use_preferred_contrast](browser/use_preferred_contrast.md) - [use_preferred_dark](browser/use_preferred_dark.md) +- [use_service_worker](browser/use_service_worker.md) # Sensors diff --git a/docs/book/src/browser/use_service_worker.md b/docs/book/src/browser/use_service_worker.md new file mode 100644 index 0000000..93f8a0a --- /dev/null +++ b/docs/book/src/browser/use_service_worker.md @@ -0,0 +1,3 @@ +# use_service_worker + + diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 930d976..6fcf5b6 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -35,6 +35,7 @@ members = [ "use_resize_observer", "use_round", "use_scroll", + "use_service_worker", "use_sorted", "use_storage", "use_throttle_fn", 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..241fda5 --- /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 = ["-c", "./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..202a47b --- /dev/null +++ b/examples/use_service_worker/post_build.sh @@ -0,0 +1,56 @@ +#!/bin/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 0000000000000000000000000000000000000000..f730574311af028829811f53861f5e48fa6c6a4a GIT binary patch literal 4308 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 BFv$P_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3041f5251b8a194f2134d25135d6a43eeda26675 GIT binary patch literal 14111 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d1e37b91f6555a30dee14a5071adcdede261fabe GIT binary patch literal 22051 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..41968977044d3238d36eca7a01d2f1ffbc63df5f GIT binary patch literal 2408 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 BNp=7L literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b5a31afb6b877cba9004090531e3db55f6105cd3 GIT binary patch literal 50089 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 literal 0 HcmV?d00001 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..2df2dcd --- /dev/null +++ b/examples/use_service_worker/src/main.rs @@ -0,0 +1,61 @@ +use leptos::*; +use leptos_use::docs::demo_or_body; +use leptos_use::{use_service_worker, use_window}; +use web_sys::HtmlMetaElement; + +#[component] +fn Demo() -> impl IntoView { + let build = load_meta_element("version") + .map(|meta| meta.content()) + .expect("'version' meta element"); + + let sw = use_service_worker(); + + view! { +

"Current build: "{build}

+ +
+ +

"registration: "{move || format!("{:#?}", sw.registration.get())}

+

"installing: "{move || sw.installing.get()}

+

"waiting: "{move || sw.waiting.get()}

+

"active: "{move || sw.active.get()}

+ +
+ + + } +} + +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: S) -> Result { + use wasm_bindgen::JsCast; + use_window() + .as_ref() + .ok_or_else(|| "No window instance!".to_owned()) + .and_then(|window| { + window + .document() + .ok_or_else(|| "No document instance!".to_owned()) + }) + .and_then(|document| { + document + .query_selector(format!("meta[name=\"{}\"]", name.as_ref()).as_str()) + .ok() + .flatten() + .ok_or_else(|| format!("Unable to find meta element with name 'version'.")) + }) + .and_then(|element| { + element.dyn_into::().map_err(|err| { + format!("Unable to cast element to HtmlMetaElement. Err: '{err:?}'.") + }) + }) +} 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/src/lib.rs b/src/lib.rs index c3384c8..4d2d979 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ mod is_none; mod is_ok; mod is_some; mod on_click_outside; +mod use_service_worker; mod signal_debounced; mod signal_throttled; mod use_active_element; @@ -74,6 +75,7 @@ pub use is_none::*; pub use is_ok::*; pub use is_some::*; pub use on_click_outside::*; +pub use use_service_worker::*; pub use signal_debounced::*; pub use signal_throttled::*; pub use use_active_element::*; diff --git a/src/use_service_worker.rs b/src/use_service_worker.rs new file mode 100644 index 0000000..17f77c3 --- /dev/null +++ b/src/use_service_worker.rs @@ -0,0 +1,269 @@ +use default_struct_builder::DefaultBuilder; +use leptos::*; +use std::borrow::Cow; +use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; +use web_sys::ServiceWorkerRegistration; + +use crate::use_window; + +/// +/// +/// ## Demo +/// +/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_service_worker) +/// +/// ## Usage +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::use_service_worker; +/// # +/// # #[component] +/// # fn Demo() -> impl IntoView { +/// # let sw = use_service_worker_with_options(UseServiceWorkerOptions { +/// # script_url: "service-worker.js".into(), +/// # skip_waiting_message: "skipWaiting".into(), +/// # ..UseServiceWorkerOptions::default() +/// # }); +/// # +/// # view! { } +/// # } +/// ``` +pub fn use_service_worker() -> UseServiceWorkerReturn { + use_service_worker_with_options(UseServiceWorkerOptions::default()) +} + +/// Version of [`use_service_worker`] that takes a `UseServiceWorkerOptions`. See [`use_service_worker`] for how to use. +pub fn use_service_worker_with_options(options: UseServiceWorkerOptions) -> UseServiceWorkerReturn { + // Reload the page whenever a new ServiceWorker is installed. + if let Some(navigator) = use_window().navigator() { + let on_controller_change = options.on_controller_change.clone(); + let reload = Closure::wrap(Box::new(move |_event: JsValue| { + on_controller_change.call(()); + }) as Box) + .into_js_value(); + navigator + .service_worker() + .set_oncontrollerchange(Some(reload.as_ref().unchecked_ref())); + } + + // Create async actions. + let create_or_update_registration = create_action_create_or_update_sw_registration(); + let get_registration = create_action_get_sw_registration(); + let update_action = create_action_update_sw(); + + // Immediately create or update the SW registration. + create_or_update_registration.dispatch(ServiceWorkerScriptUrl(options.script_url.to_string())); + + // And parse the result into individual signals. + let registration: Signal> = + Signal::derive(move || { + let a = get_registration.value().get(); + let b = create_or_update_registration.value().get(); + // We only dispatch create_or_update_registration once. Whenever we manually re-fetched the registration, the result of that has precedence! + match a { + Some(res) => res.map_err(ServiceWorkerRegistrationError::Js), + None => match b { + Some(res) => res.map_err(ServiceWorkerRegistrationError::Js), + None => Err(ServiceWorkerRegistrationError::NeverQueried), + }, + } + }); + + let fetch_registration = Closure::wrap(Box::new(move |_event: JsValue| { + get_registration.dispatch(()); + }) as Box) + .into_js_value(); + + // Handle a changing registration state. + // Notify to developer if SW registration or retrieval fails. + create_effect(move |_| { + registration.with(|reg| match reg { + Ok(registration) => { + // We must be informed when an updated SW is available. + registration.set_onupdatefound(Some(fetch_registration.as_ref().unchecked_ref())); + + // Trigger a check to see IF an updated SW is available. + update_action.dispatch(registration.clone()); + + // If a SW is installing, we must be notified if its state changes! + if let Some(sw) = registration.installing() { + sw.set_onstatechange(Some(fetch_registration.as_ref().unchecked_ref())); + } + } + Err(err) => match err { + ServiceWorkerRegistrationError::Js(err) => { + tracing::warn!("ServiceWorker registration failed: {err:?}") + } + ServiceWorkerRegistrationError::NeverQueried => {} + }, + }) + }); + + UseServiceWorkerReturn { + registration, + installing: Signal::derive(move || { + registration.with(|reg| { + reg.as_ref() + .map(|reg| reg.installing().is_some()) + .unwrap_or_default() + }) + }), + waiting: Signal::derive(move || { + registration.with(|reg| { + reg.as_ref() + .map(|reg| reg.waiting().is_some()) + .unwrap_or_default() + }) + }), + active: Signal::derive(move || { + registration.with(|reg| { + reg.as_ref() + .map(|reg| reg.active().is_some()) + .unwrap_or_default() + }) + }), + check_for_update: Callback::new(move |()| { + registration.with(|reg| { + if let Ok(reg) = reg { + update_action.dispatch(reg.clone()) + } + }) + }), + skip_waiting: Callback::new(move |()| { + registration.with_untracked(|reg| if let Ok(reg) = reg { + match reg.waiting() { + Some(sw) => { + tracing::info!("Updating to newly installed SW..."); + sw.post_message(&JsValue::from_str(&options.skip_waiting_message)).expect("post message"); + }, + None => { + tracing::warn!("You tried to update the SW while no new SW was waiting. This is probably a bug."); + }, + } + }); + }), + } +} + +/// Options for [`use_service_worker_with_options`]. +#[derive(DefaultBuilder)] +pub struct UseServiceWorkerOptions { + /// The name of your service-worker. + /// You will most likely deploy the service-worker JS fiel alongside your app. + /// A typical name is 'service-worker.js'. + pub script_url: Cow<'static, str>, + + /// The message sent to a waiting ServiceWorker when you call the `skip_waiting` callback. + pub skip_waiting_message: Cow<'static, str>, + + /// What should happen when a new service worker was activated? + /// The default implementation reloads the current page. + pub on_controller_change: Callback<()>, +} + +impl Default for UseServiceWorkerOptions { + fn default() -> Self { + Self { + script_url: "service-worker.js".into(), + skip_waiting_message: "skipWaiting".into(), + on_controller_change: Callback::new(move |()| { + use std::ops::Deref; + if let Some(window) = use_window().deref() { + match window.location().reload() { + Ok(()) => {} + Err(err) => tracing::warn!( + "Detected a ServiceWorkerController change but the page reload failed! Error: {err:?}" + ), + } + } + }), + } + } +} + +/// Return type of [`use_service_worker`]. +pub struct UseServiceWorkerReturn { + /// The current registration state. + pub registration: Signal>, + + /// Whether a SW is currently installing. + pub installing: Signal, + + /// Whether a SW was installed and is now awaiting activation. + pub waiting: Signal, + + /// Whether a SW is active. + pub active: Signal, + + /// Check for ServiceWorker update. + pub check_for_update: Callback<()>, + + /// Call this to activate a new ("waiting") SW if one is available. + pub skip_waiting: Callback<()>, +} + +struct ServiceWorkerScriptUrl(pub String); + +#[derive(Debug, Clone)] +pub enum ServiceWorkerRegistrationError { + Js(JsValue), + NeverQueried, +} + +/// A leptos action which asynchronously checks for ServiceWorker updates, given an existing ServiceWorkerRegistration. +fn create_action_update_sw( +) -> Action> { + create_action(move |registration: &ServiceWorkerRegistration| { + let registration = registration.clone(); + async move { + let update_promise = registration.update().expect("update to not fail"); + wasm_bindgen_futures::JsFuture::from(update_promise) + .await + .map(|ok| { + ok.dyn_into::() + .expect("conversion into ServiceWorkerRegistration") + }) + } + }) +} + +/// A leptos action which asynchronously creates or updates and than retrieves the ServiceWorkerRegistration. +fn create_action_create_or_update_sw_registration( +) -> Action> { + create_action(move |script_url: &ServiceWorkerScriptUrl| { + let script_url = script_url.0.to_owned(); + async move { + if let Some(navigator) = use_window().navigator() { + let promise = navigator.service_worker().register(script_url.as_str()); + wasm_bindgen_futures::JsFuture::from(promise) + .await + .map(|ok| { + ok.dyn_into::() + .expect("conversion into ServiceWorkerRegistration") + }) + } else { + Err(JsValue::from_str("no navigator")) + } + } + }) +} + +/// A leptos action which asynchronously fetches the current ServiceWorkerRegistration. +fn create_action_get_sw_registration() -> Action<(), Result> { + create_action(move |(): &()| { + async move { + if let Some(navigator) = use_window().navigator() { + let promise = navigator.service_worker().get_registration(); // Could take a scope like "/app"... + wasm_bindgen_futures::JsFuture::from(promise) + .await + .map(|ok| { + ok.dyn_into::() + .expect("conversion into ServiceWorkerRegistration") + }) + } else { + Err(JsValue::from_str("no navigator")) + } + } + }) +} From be05687ad2410848fcd782bfb3efd3b0469a2206 Mon Sep 17 00:00:00 2001 From: Lukas Potthast Date: Wed, 4 Oct 2023 17:55:57 +0200 Subject: [PATCH 05/90] Remove expect's; Rephrase comments --- src/use_service_worker.rs | 86 +++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/src/use_service_worker.rs b/src/use_service_worker.rs index 17f77c3..52beb12 100644 --- a/src/use_service_worker.rs +++ b/src/use_service_worker.rs @@ -35,22 +35,23 @@ pub fn use_service_worker() -> UseServiceWorkerReturn { /// Version of [`use_service_worker`] that takes a `UseServiceWorkerOptions`. See [`use_service_worker`] for how to use. pub fn use_service_worker_with_options(options: UseServiceWorkerOptions) -> UseServiceWorkerReturn { - // Reload the page whenever a new ServiceWorker is installed. + // Trigger the user-defined action (page-reload by default) + // whenever a new ServiceWorker is installed. if let Some(navigator) = use_window().navigator() { let on_controller_change = options.on_controller_change.clone(); - let reload = Closure::wrap(Box::new(move |_event: JsValue| { + let js_closure = Closure::wrap(Box::new(move |_event: JsValue| { on_controller_change.call(()); }) as Box) .into_js_value(); navigator .service_worker() - .set_oncontrollerchange(Some(reload.as_ref().unchecked_ref())); + .set_oncontrollerchange(Some(js_closure.as_ref().unchecked_ref())); } // Create async actions. - let create_or_update_registration = create_action_create_or_update_sw_registration(); - let get_registration = create_action_get_sw_registration(); - let update_action = create_action_update_sw(); + let create_or_update_registration = create_action_create_or_update_registration(); + let get_registration = create_action_get_registration(); + let update_sw = create_action_update(); // Immediately create or update the SW registration. create_or_update_registration.dispatch(ServiceWorkerScriptUrl(options.script_url.to_string())); @@ -60,7 +61,8 @@ pub fn use_service_worker_with_options(options: UseServiceWorkerOptions) -> UseS Signal::derive(move || { let a = get_registration.value().get(); let b = create_or_update_registration.value().get(); - // We only dispatch create_or_update_registration once. Whenever we manually re-fetched the registration, the result of that has precedence! + // We only dispatch create_or_update_registration once. + // Whenever we manually re-fetched the registration, the result of that has precedence! match a { Some(res) => res.map_err(ServiceWorkerRegistrationError::Js), None => match b { @@ -84,7 +86,7 @@ pub fn use_service_worker_with_options(options: UseServiceWorkerOptions) -> UseS registration.set_onupdatefound(Some(fetch_registration.as_ref().unchecked_ref())); // Trigger a check to see IF an updated SW is available. - update_action.dispatch(registration.clone()); + update_sw.dispatch(registration.clone()); // If a SW is installing, we must be notified if its state changes! if let Some(sw) = registration.installing() { @@ -126,7 +128,7 @@ pub fn use_service_worker_with_options(options: UseServiceWorkerOptions) -> UseS check_for_update: Callback::new(move |()| { registration.with(|reg| { if let Ok(reg) = reg { - update_action.dispatch(reg.clone()) + update_sw.dispatch(reg.clone()) } }) }), @@ -135,7 +137,9 @@ pub fn use_service_worker_with_options(options: UseServiceWorkerOptions) -> UseS match reg.waiting() { Some(sw) => { tracing::info!("Updating to newly installed SW..."); - sw.post_message(&JsValue::from_str(&options.skip_waiting_message)).expect("post message"); + if let Err(err) = sw.post_message(&JsValue::from_str(&options.skip_waiting_message)) { + tracing::warn!("Could not send message to active SW: Error: {err:?}"); + } }, None => { tracing::warn!("You tried to update the SW while no new SW was waiting. This is probably a bug."); @@ -149,12 +153,13 @@ pub fn use_service_worker_with_options(options: UseServiceWorkerOptions) -> UseS /// Options for [`use_service_worker_with_options`]. #[derive(DefaultBuilder)] pub struct UseServiceWorkerOptions { - /// The name of your service-worker. - /// You will most likely deploy the service-worker JS fiel alongside your app. - /// A typical name is 'service-worker.js'. + /// The name of your service-worker file. Must be deployed alongside your app. + /// The default name is 'service-worker.js'. pub script_url: Cow<'static, str>, /// The message sent to a waiting ServiceWorker when you call the `skip_waiting` callback. + /// The callback is part of the return type of [`use_service_worker`]! + /// The default message is 'skipWaiting'. pub skip_waiting_message: Cow<'static, str>, /// What should happen when a new service worker was activated? @@ -170,11 +175,10 @@ impl Default for UseServiceWorkerOptions { on_controller_change: Callback::new(move |()| { use std::ops::Deref; if let Some(window) = use_window().deref() { - match window.location().reload() { - Ok(()) => {} - Err(err) => tracing::warn!( + if let Err(err) = window.location().reload() { + tracing::warn!( "Detected a ServiceWorkerController change but the page reload failed! Error: {err:?}" - ), + ); } } }), @@ -196,10 +200,11 @@ pub struct UseServiceWorkerReturn { /// Whether a SW is active. pub active: Signal, - /// Check for ServiceWorker update. + /// Check for a ServiceWorker update. pub check_for_update: Callback<()>, /// Call this to activate a new ("waiting") SW if one is available. + /// Calling this while the [`UseServiceWorkerReturn::waiting`] signal resolves to false has no effect. pub skip_waiting: Callback<()>, } @@ -212,24 +217,23 @@ pub enum ServiceWorkerRegistrationError { } /// A leptos action which asynchronously checks for ServiceWorker updates, given an existing ServiceWorkerRegistration. -fn create_action_update_sw( +fn create_action_update( ) -> Action> { create_action(move |registration: &ServiceWorkerRegistration| { let registration = registration.clone(); async move { - let update_promise = registration.update().expect("update to not fail"); - wasm_bindgen_futures::JsFuture::from(update_promise) - .await - .map(|ok| { - ok.dyn_into::() - .expect("conversion into ServiceWorkerRegistration") - }) + match registration.update() { + Ok(promise) => wasm_bindgen_futures::JsFuture::from(promise) + .await + .and_then(|ok| ok.dyn_into::()), + Err(err) => Err(err), + } } }) } /// A leptos action which asynchronously creates or updates and than retrieves the ServiceWorkerRegistration. -fn create_action_create_or_update_sw_registration( +fn create_action_create_or_update_registration( ) -> Action> { create_action(move |script_url: &ServiceWorkerScriptUrl| { let script_url = script_url.0.to_owned(); @@ -238,10 +242,7 @@ fn create_action_create_or_update_sw_registration( let promise = navigator.service_worker().register(script_url.as_str()); wasm_bindgen_futures::JsFuture::from(promise) .await - .map(|ok| { - ok.dyn_into::() - .expect("conversion into ServiceWorkerRegistration") - }) + .and_then(|ok| ok.dyn_into::()) } else { Err(JsValue::from_str("no navigator")) } @@ -250,20 +251,15 @@ fn create_action_create_or_update_sw_registration( } /// A leptos action which asynchronously fetches the current ServiceWorkerRegistration. -fn create_action_get_sw_registration() -> Action<(), Result> { - create_action(move |(): &()| { - async move { - if let Some(navigator) = use_window().navigator() { - let promise = navigator.service_worker().get_registration(); // Could take a scope like "/app"... - wasm_bindgen_futures::JsFuture::from(promise) - .await - .map(|ok| { - ok.dyn_into::() - .expect("conversion into ServiceWorkerRegistration") - }) - } else { - Err(JsValue::from_str("no navigator")) - } +fn create_action_get_registration() -> Action<(), Result> { + create_action(move |(): &()| async move { + if let Some(navigator) = use_window().navigator() { + let promise = navigator.service_worker().get_registration(); + wasm_bindgen_futures::JsFuture::from(promise) + .await + .and_then(|ok| ok.dyn_into::()) + } else { + Err(JsValue::from_str("no navigator")) } }) } From 2b405b3504f37453aa22714d2f41f107bf4de598 Mon Sep 17 00:00:00 2001 From: Lukas Potthast Date: Wed, 4 Oct 2023 18:09:21 +0200 Subject: [PATCH 06/90] Reformat lib.rs --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4d2d979..6399724 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,6 @@ mod is_none; mod is_ok; mod is_some; mod on_click_outside; -mod use_service_worker; mod signal_debounced; mod signal_throttled; mod use_active_element; @@ -55,6 +54,7 @@ mod use_preferred_contrast; mod use_preferred_dark; mod use_raf_fn; mod use_scroll; +mod use_service_worker; mod use_sorted; mod use_supported; mod use_throttle_fn; @@ -75,7 +75,6 @@ pub use is_none::*; pub use is_ok::*; pub use is_some::*; pub use on_click_outside::*; -pub use use_service_worker::*; pub use signal_debounced::*; pub use signal_throttled::*; pub use use_active_element::*; @@ -105,6 +104,7 @@ pub use use_preferred_contrast::*; pub use use_preferred_dark::*; pub use use_raf_fn::*; pub use use_scroll::*; +pub use use_service_worker::*; pub use use_sorted::*; pub use use_supported::*; pub use use_throttle_fn::*; From dd2e88f8dde14e6f0408f0a0b1f16da3c3825af7 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Thu, 5 Oct 2023 12:30:51 +0200 Subject: [PATCH 07/90] cosmetic fix for book function page with examples --- docs/book/src/custom.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/book/src/custom.css b/docs/book/src/custom.css index 7627d06..fe7c8a4 100644 --- a/docs/book/src/custom.css +++ b/docs/book/src/custom.css @@ -99,4 +99,8 @@ h1 { h2, h3, h4 { font-weight: 600; +} + +#searchbar { + font-size: 1.6rem; } \ No newline at end of file From f3af2ad9ea3dc9d291c43fd13d3550bf4cfe2e52 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Sat, 21 Oct 2023 15:21:11 -0500 Subject: [PATCH 08/90] fixed some SSR issues with not using use_window() in some functions --- CHANGELOG.md | 7 +++++ Cargo.toml | 2 +- examples/ssr/Cargo.toml | 2 +- examples/ssr/src/main.rs | 6 ++-- examples/use_draggable/src/main.rs | 7 +++-- examples/use_event_listener/src/main.rs | 4 +-- src/core/mod.rs | 2 ++ src/core/ssr_safe_method.rs | 19 ++++++++++++ src/use_active_element.rs | 6 ++-- src/use_breakpoints.rs | 4 +-- src/use_document.rs | 17 +++++++---- src/use_draggable.rs | 15 ++++------ src/use_mouse.rs | 40 ++++++++----------------- src/use_window.rs | 15 +++++++--- src/utils/is.rs | 6 ++-- 15 files changed, 88 insertions(+), 64 deletions(-) create mode 100644 src/core/ssr_safe_method.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 65d75ca..c37f1ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.2] - 2023-10-21 + +### Fixes 🍕 + +- Some functions still used `window()` which could lead to panics in SSR. This is now fixed. + Specifically for `use_draggable`. + ## [0.7.1] - 2023-10-02 ### New Function 🚀 diff --git a/Cargo.toml b/Cargo.toml index 2747c15..ee22cf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos-use" -version = "0.7.1" +version = "0.7.2" edition = "2021" authors = ["Marc-Stefan Cassola"] categories = ["gui", "web-programming"] diff --git a/examples/ssr/Cargo.toml b/examples/ssr/Cargo.toml index 7b30d87..5010f37 100644 --- a/examples/ssr/Cargo.toml +++ b/examples/ssr/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "start-axum" +name = "leptos-use-ssr" version = "0.1.0" edition = "2021" diff --git a/examples/ssr/src/main.rs b/examples/ssr/src/main.rs index 6b92404..c08d00e 100644 --- a/examples/ssr/src/main.rs +++ b/examples/ssr/src/main.rs @@ -5,8 +5,8 @@ async fn main() { use leptos::logging::log; use leptos::*; use leptos_axum::{generate_route_list, LeptosRoutes}; - use start_axum::app::*; - use start_axum::fileserv::file_and_error_handler; + use leptos_use_ssr::app::*; + use leptos_use_ssr::fileserv::file_and_error_handler; simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging"); @@ -18,7 +18,7 @@ async fn main() { let conf = get_configuration(None).await.unwrap(); let leptos_options = conf.leptos_options; let addr = leptos_options.site_addr; - let routes = generate_route_list(|| view! { }).await; + let routes = generate_route_list(|| view! { }); // build our application with a route let app = Router::new() diff --git a/examples/use_draggable/src/main.rs b/examples/use_draggable/src/main.rs index cf92cdb..9ed1bfb 100644 --- a/examples/use_draggable/src/main.rs +++ b/examples/use_draggable/src/main.rs @@ -2,13 +2,16 @@ use leptos::html::Div; use leptos::*; use leptos_use::core::Position; use leptos_use::docs::demo_or_body; -use leptos_use::{use_draggable_with_options, UseDraggableOptions, UseDraggableReturn}; +use leptos_use::{use_draggable_with_options, use_window, UseDraggableOptions, UseDraggableReturn}; #[component] fn Demo() -> impl IntoView { let el = create_node_ref::
(); - let inner_width = window().inner_width().unwrap().as_f64().unwrap(); + let inner_width = use_window() + .as_ref() + .map(|w| w.inner_width().unwrap().as_f64().unwrap()) + .unwrap_or(0.0); let UseDraggableReturn { x, y, style, .. } = use_draggable_with_options( el, 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/src/core/mod.rs b/src/core/mod.rs index 2a8c2e0..ea1f1b7 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -5,6 +5,7 @@ mod maybe_rw_signal; mod pointer_type; mod position; mod size; +mod ssr_safe_method; mod storage; pub use connection_ready_state::*; @@ -14,4 +15,5 @@ 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/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_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..5954243 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()) @@ -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_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_window.rs b/src/use_window.rs index b9dae5a..b0185f8 100644 --- a/src/use_window.rs +++ b/src/use_window.rs @@ -1,3 +1,4 @@ +use crate::core::impl_ssr_safe_method; use crate::{use_document, UseDocument}; use cfg_if::cfg_if; use std::ops::Deref; @@ -48,14 +49,20 @@ impl Deref for UseWindow { } impl UseWindow { - /// Returns the `Some(Navigator)` in the Browser. `None` otherwise. - pub fn navigator(&self) -> Option { - self.0.as_ref().map(|w| w.navigator()) - } + impl_ssr_safe_method!( + /// Returns `Some(Navigator)` in the Browser. `None` otherwise. + navigator(&self) -> Option + ); /// Returns the same as [`use_document`]. #[inline(always)] pub fn document(&self) -> UseDocument { use_document() } + + impl_ssr_safe_method!( + /// Returns the same as `window().match_media()` in the Browser. `Ok(None)` otherwise. + match_media(&self, query: &str) -> Result, wasm_bindgen::JsValue>; + .unwrap_or(Ok(None)) + ); } diff --git a/src/utils/is.rs b/src/utils/is.rs index 2fb045c..3655883 100644 --- a/src/utils/is.rs +++ b/src/utils/is.rs @@ -1,8 +1,10 @@ +use crate::use_window; use lazy_static::lazy_static; -use leptos::*; lazy_static! { - pub static ref IS_IOS: bool = if let Ok(user_agent) = window().navigator().user_agent() { + pub static ref IS_IOS: bool = if let Some(Ok(user_agent)) = + use_window().navigator().map(|n| n.user_agent()) + { user_agent.contains("iPhone") || user_agent.contains("iPad") || user_agent.contains("iPod") } else { false From b8421f218616599b438eff858e466ae5755fe2aa Mon Sep 17 00:00:00 2001 From: Maccesch Date: Mon, 23 Oct 2023 20:15:05 -0500 Subject: [PATCH 09/90] added use_infinite_scroll --- CHANGELOG.md | 14 + Cargo.toml | 112 +++--- docs/book/src/SUMMARY.md | 1 + docs/book/src/sensors/use_infinite_scroll.md | 3 + examples/Cargo.toml | 1 + examples/signal_debounced/src/main.rs | 14 +- examples/signal_throttled/src/main.rs | 14 +- examples/ssr/src/app.rs | 24 +- examples/use_active_element/src/main.rs | 6 +- examples/use_draggable/src/main.rs | 10 +- examples/use_drop_zone/src/main.rs | 40 +-- examples/use_geolocation/src/main.rs | 16 +- examples/use_idle/src/main.rs | 15 +- examples/use_infinite_scroll/Cargo.toml | 16 + examples/use_infinite_scroll/README.md | 23 ++ examples/use_infinite_scroll/Trunk.toml | 2 + examples/use_infinite_scroll/index.html | 7 + examples/use_infinite_scroll/input.css | 3 + .../use_infinite_scroll/rust-toolchain.toml | 2 + examples/use_infinite_scroll/src/main.rs | 40 +++ examples/use_infinite_scroll/style/output.css | 338 ++++++++++++++++++ .../use_infinite_scroll/tailwind.config.js | 15 + examples/use_mouse/src/main.rs | 12 +- examples/use_raf_fn/src/main.rs | 5 +- examples/use_timestamp/src/main.rs | 7 +- examples/use_window_scroll/src/main.rs | 16 +- src/core/direction.rs | 41 +++ src/core/mod.rs | 2 + src/lib.rs | 2 + src/use_infinite_scroll.rs | 250 +++++++++++++ src/use_scroll.rs | 81 +++-- 31 files changed, 920 insertions(+), 212 deletions(-) create mode 100644 docs/book/src/sensors/use_infinite_scroll.md create mode 100644 examples/use_infinite_scroll/Cargo.toml create mode 100644 examples/use_infinite_scroll/README.md create mode 100644 examples/use_infinite_scroll/Trunk.toml create mode 100644 examples/use_infinite_scroll/index.html create mode 100644 examples/use_infinite_scroll/input.css create mode 100644 examples/use_infinite_scroll/rust-toolchain.toml create mode 100644 examples/use_infinite_scroll/src/main.rs create mode 100644 examples/use_infinite_scroll/style/output.css create mode 100644 examples/use_infinite_scroll/tailwind.config.js create mode 100644 src/core/direction.rs create mode 100644 src/use_infinite_scroll.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c37f1ad..23dd824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] - + +### New Functions 🚀 + +- `use_infinite_scroll` + +### Breaking Changes 🛠 + +- `use_scroll` returns `impl Fn(T) + Clone` instead of `Box`. + +### Other Changes 🔥 + +- `UseScrollReturn` is now documented + ## [0.7.2] - 2023-10-21 ### Fixes 🍕 diff --git a/Cargo.toml b/Cargo.toml index ee22cf8..582fa0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,69 +13,71 @@ repository = "https://github.com/Synphonyte/leptos-use" homepage = "https://leptos-use.rs" [dependencies] -leptos = "0.5" -wasm-bindgen = "0.2" -js-sys = "0.3" +cfg-if = "1" default-struct-builder = "0.5" +futures-util = "0.3" +gloo-timers = { version = "0.3.0", features = ["futures"] } +js-sys = "0.3" +lazy_static = "1" +leptos = "0.5" num = { version = "0.4", optional = true } +paste = "1" serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } -paste = "1" -lazy_static = "1" -cfg-if = "1" +wasm-bindgen = "0.2" [dependencies.web-sys] version = "0.3" features = [ - "AddEventListenerOptions", - "BinaryType", - "Coordinates", - "CloseEvent", - "CssStyleDeclaration", - "CustomEvent", - "CustomEventInit", - "DomRect", - "DomRectReadOnly", - "DataTransfer", - "DragEvent", - "Element", - "EventListener", - "EventListenerOptions", - "EventTarget", - "File", - "FileList", - "Geolocation", - "HtmlElement", - "HtmlLinkElement", - "HtmlStyleElement", - "IntersectionObserver", - "IntersectionObserverInit", - "IntersectionObserverEntry", - "MediaQueryList", - "MouseEvent", - "MutationObserver", - "MutationObserverInit", - "MutationRecord", - "Navigator", - "NodeList", - "PointerEvent", - "Position", - "PositionError", - "PositionOptions", - "ResizeObserver", - "ResizeObserverBoxOptions", - "ResizeObserverEntry", - "ResizeObserverOptions", - "ResizeObserverSize", - "ScrollBehavior", - "ScrollToOptions", - "Storage", - "Touch", - "TouchEvent", - "TouchList", - "VisibilityState", - "WebSocket", - "Window", + "AddEventListenerOptions", + "BinaryType", + "Coordinates", + "CloseEvent", + "CssStyleDeclaration", + "CustomEvent", + "CustomEventInit", + "DomRect", + "DomRectReadOnly", + "DataTransfer", + "DragEvent", + "Element", + "EventListener", + "EventListenerOptions", + "EventTarget", + "File", + "FileList", + "Geolocation", + "HtmlElement", + "HtmlLinkElement", + "HtmlStyleElement", + "IntersectionObserver", + "IntersectionObserverInit", + "IntersectionObserverEntry", + "MediaQueryList", + "MouseEvent", + "MutationObserver", + "MutationObserverInit", + "MutationRecord", + "Navigator", + "NodeList", + "PointerEvent", + "Position", + "PositionError", + "PositionOptions", + "ResizeObserver", + "ResizeObserverBoxOptions", + "ResizeObserverEntry", + "ResizeObserverOptions", + "ResizeObserverSize", + "ScrollBehavior", + "ScrollToOptions", + "Storage", + "Touch", + "TouchEvent", + "TouchList", + "VisibilityState", + "WebSocket", + "Window", ] [features] diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index dbb23c8..ea74e15 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -45,6 +45,7 @@ - [use_element_hover](sensors/use_element_hover.md) - [use_geolocation](sensors/use_geolocation.md) - [use_idle](sensors/use_idle.md) +- [use_infinite_scroll](sensors/use_infinite_scroll.md) - [use_mouse](sensors/use_mouse.md) - [use_scroll](sensors/use_scroll.md) diff --git a/docs/book/src/sensors/use_infinite_scroll.md b/docs/book/src/sensors/use_infinite_scroll.md new file mode 100644 index 0000000..1810384 --- /dev/null +++ b/docs/book/src/sensors/use_infinite_scroll.md @@ -0,0 +1,3 @@ +# use_infinite_scroll + + diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 930d976..d367ba8 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -24,6 +24,7 @@ members = [ "use_floor", "use_geolocation", "use_idle", + "use_infinite_scroll", "use_intersection_observer", "use_interval", "use_interval_fn", diff --git a/examples/signal_debounced/src/main.rs b/examples/signal_debounced/src/main.rs index 4e42d6c..cce81fc 100644 --- a/examples/signal_debounced/src/main.rs +++ b/examples/signal_debounced/src/main.rs @@ -15,17 +15,9 @@ fn Demo() -> impl IntoView { on:input=move |event| set_input(event_target_value(&event)) placeholder="Try to type quickly, then stop..." /> - - Delay is set to 1000ms for this demo. - -

- Input signal: - {input} -

-

- Debounced signal: - {debounced} -

+ Delay is set to 1000ms for this demo. +

Input signal: {input}

+

Debounced signal: {debounced}

} } diff --git a/examples/signal_throttled/src/main.rs b/examples/signal_throttled/src/main.rs index 2ff7366..0fc9058 100644 --- a/examples/signal_throttled/src/main.rs +++ b/examples/signal_throttled/src/main.rs @@ -15,17 +15,9 @@ fn Demo() -> impl IntoView { on:input=move |event| set_input(event_target_value(&event)) placeholder="Try to type quickly..." /> - - Delay is set to 1000ms for this demo. - -

- Input signal: - {input} -

-

- Throttled signal: - {throttled} -

+ Delay is set to 1000ms for this demo. +

Input signal: {input}

+

Throttled signal: {throttled}

} } diff --git a/examples/ssr/src/app.rs b/examples/ssr/src/app.rs index 91e95b8..29e62fc 100644 --- a/examples/ssr/src/app.rs +++ b/examples/ssr/src/app.rs @@ -64,24 +64,10 @@ fn HomePage() -> impl IntoView { debounced_fn(); view! { -

- Leptos-Use SSR Example -

- -

- Locale zh-Hans-CN-u-nu-hanidec: - {zh_count} -

-

- Press any key: - {key} -

-

- Debounced called: - {debounce_value} -

+

Leptos-Use SSR Example

+ +

Locale zh-Hans-CN-u-nu-hanidec: {zh_count}

+

Press any key: {key}

+

Debounced called: {debounce_value}

} } diff --git a/examples/use_active_element/src/main.rs b/examples/use_active_element/src/main.rs index e18fe28..8678475 100644 --- a/examples/use_active_element/src/main.rs +++ b/examples/use_active_element/src/main.rs @@ -19,11 +19,7 @@ fn Demo() -> impl IntoView { "Select the inputs below to see the changes"
- + diff --git a/examples/use_draggable/src/main.rs b/examples/use_draggable/src/main.rs index 9ed1bfb..f7cec80 100644 --- a/examples/use_draggable/src/main.rs +++ b/examples/use_draggable/src/main.rs @@ -24,20 +24,14 @@ fn Demo() -> impl IntoView { ); view! { -

- Check the floating box -

+

Check the floating box

"👋 Drag me!" -
- I am - {move || x().round()}, - {move || y().round()} -
+
I am {move || x().round()} , {move || y().round()}
} } diff --git a/examples/use_drop_zone/src/main.rs b/examples/use_drop_zone/src/main.rs index d70c460..52f31d4 100644 --- a/examples/use_drop_zone/src/main.rs +++ b/examples/use_drop_zone/src/main.rs @@ -22,45 +22,21 @@ fn Demo() -> impl IntoView { view! {
-

- Drop files into dropZone -

+

Drop files into dropZone

Drop me
-
- 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_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() }} , 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! { +
+ +
{item}
+
+
+ } +} + +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}
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_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/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/mod.rs b/src/core/mod.rs index ea1f1b7..25d7d04 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,4 +1,5 @@ mod connection_ready_state; +mod direction; mod element_maybe_signal; mod elements_maybe_signal; mod maybe_rw_signal; @@ -9,6 +10,7 @@ mod ssr_safe_method; mod storage; pub use connection_ready_state::*; +pub use direction::*; pub use element_maybe_signal::*; pub use elements_maybe_signal::*; pub use maybe_rw_signal::*; diff --git a/src/lib.rs b/src/lib.rs index c3384c8..aaea588 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ 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; @@ -92,6 +93,7 @@ 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::*; diff --git a/src/use_infinite_scroll.rs b/src/use_infinite_scroll.rs new file mode 100644 index 0000000..8153c4f --- /dev/null +++ b/src/use_infinite_scroll.rs @@ -0,0 +1,250 @@ +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 { + if !is_loading.get_untracked() { + set_loading.set(true); + + let state = state.clone(); + 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.map(|v| *v).unwrap_or_default() { + measure(); + } + }, + true, + ); + + let _ = watch( + move || state.arrived_state.get().get_direction(direction), + move |_, _, _| { + 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_scroll.rs b/src/use_scroll.rs index 94c2c12..d25467b 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,10 +248,10 @@ 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); @@ -417,13 +422,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 +505,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, - pub set_y: Box, + + /// Sets the value of `y`. This does also scroll the element. + pub set_y: SetYFn, + + /// 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, - pub measure: Box, + + /// Re-evaluates the `arrived_state`. + pub measure: MFn, } -#[derive(Copy, Clone)] -pub struct Directions { - pub left: bool, - pub right: bool, - pub top: bool, - pub bottom: bool, -} - -#[derive(Default, Copy, Clone)] +#[derive(Default, Copy, Clone, Debug)] /// Threshold in pixels when we consider a side to have arrived (`UseScrollReturn::arrived_state`). pub struct ScrollOffset { pub left: f64, @@ -528,3 +545,17 @@ pub struct ScrollOffset { pub right: f64, pub bottom: f64, } + +impl ScrollOffset { + /// Sets the value of the provided direction + pub fn set_direction(mut self, direction: Direction, value: f64) -> Self { + match direction { + Direction::Top => self.top = value, + Direction::Bottom => self.bottom = value, + Direction::Left => self.left = value, + Direction::Right => self.right = value, + } + + self + } +} From cd67ca5542730c42af2ee1ed8c78ea2deb4a10e8 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Mon, 23 Oct 2023 23:49:11 -0500 Subject: [PATCH 10/90] added use_web_notification --- CHANGELOG.md | 1 + Cargo.toml | 5 + docs/book/src/SUMMARY.md | 1 + docs/book/src/browser/use_web_notification.md | 3 + examples/Cargo.toml | 1 + examples/use_web_notification/Cargo.toml | 16 + examples/use_web_notification/README.md | 23 + examples/use_web_notification/Trunk.toml | 2 + examples/use_web_notification/index.html | 7 + examples/use_web_notification/input.css | 3 + .../use_web_notification/rust-toolchain.toml | 2 + examples/use_web_notification/src/main.rs | 55 +++ .../use_web_notification/style/output.css | 289 ++++++++++++ .../use_web_notification/tailwind.config.js | 15 + src/lib.rs | 2 + src/use_web_notification.rs | 435 ++++++++++++++++++ 16 files changed, 860 insertions(+) create mode 100644 docs/book/src/browser/use_web_notification.md create mode 100644 examples/use_web_notification/Cargo.toml create mode 100644 examples/use_web_notification/README.md create mode 100644 examples/use_web_notification/Trunk.toml create mode 100644 examples/use_web_notification/index.html create mode 100644 examples/use_web_notification/input.css create mode 100644 examples/use_web_notification/rust-toolchain.toml create mode 100644 examples/use_web_notification/src/main.rs create mode 100644 examples/use_web_notification/style/output.css create mode 100644 examples/use_web_notification/tailwind.config.js create mode 100644 src/use_web_notification.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 23dd824..e7a84dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New Functions 🚀 +- `use_web_notification` - thanks for the help @centershocks44 - `use_infinite_scroll` ### Breaking Changes 🛠 diff --git a/Cargo.toml b/Cargo.toml index 582fa0a..9545c23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ paste = "1" serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" [dependencies.web-sys] version = "0.3" @@ -60,6 +61,10 @@ features = [ "MutationRecord", "Navigator", "NodeList", + "Notification", + "NotificationDirection", + "NotificationOptions", + "NotificationPermission", "PointerEvent", "Position", "PositionError", diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index ea74e15..d33f60d 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -38,6 +38,7 @@ - [use_media_query](browser/use_media_query.md) - [use_preferred_contrast](browser/use_preferred_contrast.md) - [use_preferred_dark](browser/use_preferred_dark.md) +- [use_web_notification](browser/use_web_notification.md) # Sensors diff --git a/docs/book/src/browser/use_web_notification.md b/docs/book/src/browser/use_web_notification.md new file mode 100644 index 0000000..ceef60c --- /dev/null +++ b/docs/book/src/browser/use_web_notification.md @@ -0,0 +1,3 @@ +# use_web_notification + + diff --git a/examples/Cargo.toml b/examples/Cargo.toml index d367ba8..c5dbe1a 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -40,6 +40,7 @@ members = [ "use_storage", "use_throttle_fn", "use_timestamp", + "use_web_notification", "use_websocket", "use_window_focus", "use_window_scroll", 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..b9045bc --- /dev/null +++ b/examples/use_web_notification/src/main.rs @@ -0,0 +1,55 @@ +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! { +
+

+ Supported: +

+
+ + The Notification Web API is not supported in your browser.
+ } + > + + + } +} + +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/src/lib.rs b/src/lib.rs index aaea588..8530bca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ mod is_none; mod is_ok; mod is_some; mod on_click_outside; +mod use_web_notification; mod signal_debounced; mod signal_throttled; mod use_active_element; @@ -75,6 +76,7 @@ pub use is_none::*; pub use is_ok::*; pub use is_some::*; pub use on_click_outside::*; +pub use use_web_notification::*; pub use signal_debounced::*; pub use signal_throttled::*; pub use use_active_element::*; diff --git a/src/use_web_notification.rs b/src/use_web_notification.rs new file mode 100644 index 0000000..92a7024 --- /dev/null +++ b/src/use_web_notification.rs @@ -0,0 +1,435 @@ +use crate::{use_event_listener, use_supported, use_window}; +use default_struct_builder::DefaultBuilder; +use leptos::ev::visibilitychange; +use leptos::*; +use std::rc::Rc; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::JsCast; + +/// Reactive [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/Notification). +/// +/// The Web Notification interface of the Notifications API is used to configure and display desktop notifications to the user. +/// +/// ## Demo +/// +/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_web_notification) +/// +/// ## Usage +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::{use_web_notification_with_options, UseWebNotificationOptions, ShowOptions, UseWebNotificationReturn, NotificationDirection}; +/// # +/// # #[component] +/// # fn Demo() -> impl IntoView { +/// let UseWebNotificationReturn { +/// show, +/// close, +/// .. +/// } = use_web_notification_with_options( +/// UseWebNotificationOptions::default() +/// .direction(NotificationDirection::Auto) +/// .language("en") +/// .tag("test"), +/// ); +/// +/// show(ShowOptions::default().title("Hello World from leptos-use")); +/// # +/// # view! { } +/// # } +/// ``` +pub fn use_web_notification( +) -> UseWebNotificationReturn { + use_web_notification_with_options(UseWebNotificationOptions::default()) +} + +/// Version of [`use_web_notification`] which takes an [`UseWebNotificationOptions`]. +pub fn use_web_notification_with_options( + options: UseWebNotificationOptions, +) -> UseWebNotificationReturn { + let is_supported = use_supported(browser_supports_notifications); + + let (notification, set_notification) = create_signal(None::); + + let (permission, set_permission) = create_signal(NotificationPermission::default()); + + let on_click_closure = Closure::::new({ + let on_click = Rc::clone(&options.on_click); + move |e: web_sys::Event| { + on_click(e); + } + }) + .into_js_value(); + + let on_close_closure = Closure::::new({ + let on_close = Rc::clone(&options.on_close); + move |e: web_sys::Event| { + on_close(e); + } + }) + .into_js_value(); + + let on_error_closure = Closure::::new({ + let on_error = Rc::clone(&options.on_error); + move |e: web_sys::Event| { + on_error(e); + } + }) + .into_js_value(); + + let on_show_closure = Closure::::new({ + let on_show = Rc::clone(&options.on_show); + move |e: web_sys::Event| { + on_show(e); + } + }) + .into_js_value(); + + let show = { + let options = options.clone(); + let on_click_closure = on_click_closure.clone(); + let on_close_closure = on_close_closure.clone(); + let on_error_closure = on_error_closure.clone(); + let on_show_closure = on_show_closure.clone(); + + move |options_override: ShowOptions| { + if !is_supported.get_untracked() { + return; + } + + let options = options.clone(); + let on_click_closure = on_click_closure.clone(); + let on_close_closure = on_close_closure.clone(); + let on_error_closure = on_error_closure.clone(); + let on_show_closure = on_show_closure.clone(); + + spawn_local(async move { + set_permission.set(request_web_notification_permission().await); + + let mut notification_options = web_sys::NotificationOptions::from(&options); + options_override.override_notification_options(&mut notification_options); + + let notification_value = web_sys::Notification::new_with_options( + &options_override.title.unwrap_or(options.title), + ¬ification_options, + ) + .expect("Notification should be created"); + + notification_value.set_onclick(Some(on_click_closure.unchecked_ref())); + notification_value.set_onclose(Some(on_close_closure.unchecked_ref())); + notification_value.set_onerror(Some(on_error_closure.unchecked_ref())); + notification_value.set_onshow(Some(on_show_closure.unchecked_ref())); + + set_notification.set(Some(notification_value)); + }); + } + }; + + let close = { + move || { + notification.with_untracked(|notification| { + if let Some(notification) = notification { + notification.close(); + } + }); + set_notification.set(None); + } + }; + + spawn_local(async move { + set_permission.set(request_web_notification_permission().await); + }); + + on_cleanup(close.clone()); + + // Use close() to remove a notification that is no longer relevant to to + // the user (e.g.the user already read the notification on the webpage). + // Most modern browsers dismiss notifications automatically after a few + // moments(around four seconds). + if is_supported.get_untracked() { + let _ = use_event_listener(document(), visibilitychange, move |e: web_sys::Event| { + e.prevent_default(); + if document().visibility_state() == web_sys::VisibilityState::Visible { + // The tab has become visible so clear the now-stale Notification: + close() + } + }); + } + + UseWebNotificationReturn { + is_supported: is_supported.into(), + notification: notification.into(), + show, + close, + permission: permission.into(), + } +} + +#[derive(Default, Clone, Copy, Eq, PartialEq, Debug)] +pub enum NotificationDirection { + #[default] + Auto, + LeftToRight, + RightToLeft, +} + +impl From for web_sys::NotificationDirection { + fn from(direction: NotificationDirection) -> Self { + match direction { + NotificationDirection::Auto => Self::Auto, + NotificationDirection::LeftToRight => Self::Ltr, + NotificationDirection::RightToLeft => Self::Rtl, + } + } +} + +/// Options for [`use_web_notification_with_options`]. +/// See [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/notification) for more info. +/// +/// The following implementations are missing: +/// - `renotify` +/// - `vibrate` +/// - `silent` +/// - `image` +#[derive(DefaultBuilder, Clone)] +pub struct UseWebNotificationOptions { + /// The title property of the Notification interface indicates + /// the title of the notification + #[builder(into)] + title: String, + + /// The body string of the notification as specified in the constructor's + /// options parameter. + #[builder(into)] + body: Option, + + /// The text direction of the notification as specified in the constructor's + /// options parameter. Can be `LeftToRight`, `RightToLeft` or `Auto` (default). + /// See [`web_sys::NotificationDirection`] for more info. + direction: NotificationDirection, + + /// The language code of the notification as specified in the constructor's + /// options parameter. + #[builder(into)] + language: Option, + + /// The ID of the notification(if any) as specified in the constructor's options + /// parameter. + #[builder(into)] + tag: Option, + + /// The URL of the image used as an icon of the notification as specified + /// in the constructor's options parameter. + #[builder(into)] + icon: Option, + + /// A boolean value indicating that a notification should remain active until the + /// user clicks or dismisses it, rather than closing automatically. + require_interaction: bool, + + // /// A boolean value specifying whether the user should be notified after a new notification replaces an old one. + // /// The default is `false`, which means they won't be notified. If `true`, then `tag` also must be set. + // #[builder(into)] + // renotify: bool, + /// Called when the user clicks on displayed `Notification`. + on_click: Rc, + + /// Called when the user closes a `Notification`. + on_close: Rc, + + /// Called when something goes wrong with a `Notification` + /// (in many cases an error preventing the notification from being displayed.) + on_error: Rc, + + /// Called when a `Notification` is displayed + on_show: Rc, +} + +impl Default for UseWebNotificationOptions { + fn default() -> Self { + Self { + title: "".to_string(), + body: None, + direction: NotificationDirection::default(), + language: None, + tag: None, + icon: None, + require_interaction: false, + // renotify: false, + on_click: Rc::new(|_| {}), + on_close: Rc::new(|_| {}), + on_error: Rc::new(|_| {}), + on_show: Rc::new(|_| {}), + } + } +} + +impl From<&UseWebNotificationOptions> for web_sys::NotificationOptions { + fn from(options: &UseWebNotificationOptions) -> Self { + let mut web_sys_options = Self::new(); + + web_sys_options + .dir(options.direction.into()) + .require_interaction(options.require_interaction); + // .renotify(options.renotify); + + if let Some(body) = &options.body { + web_sys_options.body(&body); + } + + if let Some(icon) = &options.icon { + web_sys_options.icon(&icon); + } + + if let Some(language) = &options.language { + web_sys_options.lang(&language); + } + + if let Some(tag) = &options.tag { + web_sys_options.tag(&tag); + } + + web_sys_options + } +} + +/// Options for [`UseWebNotificationReturn::show`]. +/// This can be used to override options passed to [`use_web_notification`]. +/// See [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/notification) for more info. +/// +/// The following implementations are missing: +/// - `vibrate` +/// - `silent` +/// - `image` +#[derive(DefaultBuilder, Default)] +pub struct ShowOptions { + /// The title property of the Notification interface indicates + /// the title of the notification + #[builder(into)] + title: Option, + + /// The body string of the notification as specified in the constructor's + /// options parameter. + #[builder(into)] + body: Option, + + /// The text direction of the notification as specified in the constructor's + /// options parameter. Can be `LeftToRight`, `RightToLeft` or `Auto` (default). + /// See [`web_sys::NotificationDirection`] for more info. + #[builder(into)] + direction: Option, + + /// The language code of the notification as specified in the constructor's + /// options parameter. + #[builder(into)] + language: Option, + + /// The ID of the notification(if any) as specified in the constructor's options + /// parameter. + #[builder(into)] + tag: Option, + + /// The URL of the image used as an icon of the notification as specified + /// in the constructor's options parameter. + #[builder(into)] + icon: Option, + + /// A boolean value indicating that a notification should remain active until the + /// user clicks or dismisses it, rather than closing automatically. + #[builder(into)] + require_interaction: Option, + // /// A boolean value specifying whether the user should be notified after a new notification replaces an old one. + // /// The default is `false`, which means they won't be notified. If `true`, then `tag` also must be set. + // #[builder(into)] + // renotify: Option, +} + +impl ShowOptions { + fn override_notification_options(&self, options: &mut web_sys::NotificationOptions) { + if let Some(direction) = self.direction { + options.dir(direction.into()); + } + + if let Some(require_interaction) = self.require_interaction { + options.require_interaction(require_interaction); + } + + if let Some(body) = &self.body { + options.body(body); + } + + if let Some(icon) = &self.icon { + options.icon(icon); + } + + if let Some(language) = &self.language { + options.lang(language); + } + + if let Some(tag) = &self.tag { + options.tag(tag); + } + + // if let Some(renotify) = &self.renotify { + // options.renotify(renotify); + // } + } +} + +/// Helper function to determine if browser supports notifications +fn browser_supports_notifications() -> bool { + if let Some(window) = use_window().as_ref() { + if window.has_own_property(&wasm_bindgen::JsValue::from_str("Notification")) { + return true; + } + } + + false +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +/// The permission to send notifications +pub enum NotificationPermission { + /// Notification has not been requested. In effect this is the same as `Denied`. + #[default] + Default, + /// You are allowed to send notifications + Granted, + /// You are *not* allowed to send notifications + Denied, +} + +impl From for NotificationPermission { + fn from(permission: web_sys::NotificationPermission) -> Self { + match permission { + web_sys::NotificationPermission::Default => Self::Default, + web_sys::NotificationPermission::Granted => Self::Granted, + web_sys::NotificationPermission::Denied => Self::Denied, + web_sys::NotificationPermission::__Nonexhaustive => Self::Default, + } + } +} + +/// Use `window.Notification.requestPosition()`. Returns a future that should be awaited +/// at least once before using [`use_web_notification`] to make sure +/// you have the permission to send notifications. +async fn request_web_notification_permission() -> NotificationPermission { + if let Ok(notification_permission) = web_sys::Notification::request_permission() { + let _ = wasm_bindgen_futures::JsFuture::from(notification_permission).await; + } + + web_sys::Notification::permission().into() +} + +/// Return type for [`use_web_notification`]. +pub struct UseWebNotificationReturn +where + ShowFn: Fn(ShowOptions) + Clone, + CloseFn: Fn() + Clone, +{ + pub is_supported: Signal, + pub notification: Signal>, + pub show: ShowFn, + pub close: CloseFn, + pub permission: Signal, +} From 96cc7f73994df866a5137d4e4e1ce3dee6101e4c Mon Sep 17 00:00:00 2001 From: Maccesch Date: Tue, 24 Oct 2023 00:36:32 -0500 Subject: [PATCH 11/90] made use_service_worker more in line with the other functions --- .idea/leptos-use.iml | 1 + examples/use_service_worker/src/main.rs | 58 +++++++++---------- src/use_service_worker.rs | 76 ++++++++++++++----------- 3 files changed, 73 insertions(+), 62 deletions(-) diff --git a/.idea/leptos-use.iml b/.idea/leptos-use.iml index df1b62f..bb3717d 100644 --- a/.idea/leptos-use.iml +++ b/.idea/leptos-use.iml @@ -57,6 +57,7 @@ + diff --git a/examples/use_service_worker/src/main.rs b/examples/use_service_worker/src/main.rs index 2df2dcd..93dc1fa 100644 --- a/examples/use_service_worker/src/main.rs +++ b/examples/use_service_worker/src/main.rs @@ -1,6 +1,6 @@ use leptos::*; -use leptos_use::docs::demo_or_body; -use leptos_use::{use_service_worker, use_window}; +use leptos_use::docs::{demo_or_body, BooleanDisplay}; +use leptos_use::{use_document, use_service_worker, UseServiceWorkerReturn}; use web_sys::HtmlMetaElement; #[component] @@ -9,21 +9,28 @@ fn Demo() -> impl IntoView { .map(|meta| meta.content()) .expect("'version' meta element"); - let sw = use_service_worker(); + let UseServiceWorkerReturn { + registration, + installing, + waiting, + active, + skip_waiting, + .. + } = use_service_worker(); view! { -

"Current build: "{build}

+

"Current build: " {build}


-

"registration: "{move || format!("{:#?}", sw.registration.get())}

-

"installing: "{move || sw.installing.get()}

-

"waiting: "{move || sw.waiting.get()}

-

"active: "{move || sw.active.get()}

+

"registration: " {move || format!("{:#?}", registration())}

+

"installing: "

+

"waiting: "

+

"active: "


- + } } @@ -36,26 +43,17 @@ fn main() { }) } -fn load_meta_element>(name: S) -> Result { +fn load_meta_element(name: &str) -> Result { use wasm_bindgen::JsCast; - use_window() - .as_ref() - .ok_or_else(|| "No window instance!".to_owned()) - .and_then(|window| { - window - .document() - .ok_or_else(|| "No document instance!".to_owned()) - }) - .and_then(|document| { - document - .query_selector(format!("meta[name=\"{}\"]", name.as_ref()).as_str()) - .ok() - .flatten() - .ok_or_else(|| format!("Unable to find meta element with name 'version'.")) - }) - .and_then(|element| { - element.dyn_into::().map_err(|err| { - format!("Unable to cast element to HtmlMetaElement. Err: '{err:?}'.") - }) - }) + 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/src/use_service_worker.rs b/src/use_service_worker.rs index 52beb12..ec2d262 100644 --- a/src/use_service_worker.rs +++ b/src/use_service_worker.rs @@ -1,46 +1,52 @@ use default_struct_builder::DefaultBuilder; use leptos::*; -use std::borrow::Cow; +use std::rc::Rc; use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; use web_sys::ServiceWorkerRegistration; use crate::use_window; +/// Reactive [ServiceWorker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API). /// -/// -/// ## Demo -/// -/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_service_worker) +/// Please check the [working example](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_service_worker). /// /// ## Usage /// /// ``` /// # use leptos::*; -/// # use leptos_use::use_service_worker; +/// # use leptos_use::{use_service_worker_with_options, UseServiceWorkerOptions, UseServiceWorkerReturn}; /// # /// # #[component] /// # fn Demo() -> impl IntoView { -/// # let sw = use_service_worker_with_options(UseServiceWorkerOptions { -/// # script_url: "service-worker.js".into(), -/// # skip_waiting_message: "skipWaiting".into(), -/// # ..UseServiceWorkerOptions::default() -/// # }); -/// # -/// # view! { } +/// let UseServiceWorkerReturn { +/// registration, +/// installing, +/// waiting, +/// active, +/// skip_waiting, +/// check_for_update, +/// } = use_service_worker_with_options(UseServiceWorkerOptions::default() +/// .script_url("service-worker.js") +/// .skip_waiting_message("skipWaiting"), +/// ); +/// +/// # view! { } /// # } /// ``` -pub fn use_service_worker() -> UseServiceWorkerReturn { +pub fn use_service_worker() -> UseServiceWorkerReturn { use_service_worker_with_options(UseServiceWorkerOptions::default()) } /// Version of [`use_service_worker`] that takes a `UseServiceWorkerOptions`. See [`use_service_worker`] for how to use. -pub fn use_service_worker_with_options(options: UseServiceWorkerOptions) -> UseServiceWorkerReturn { +pub fn use_service_worker_with_options( + options: UseServiceWorkerOptions, +) -> UseServiceWorkerReturn { // Trigger the user-defined action (page-reload by default) // whenever a new ServiceWorker is installed. if let Some(navigator) = use_window().navigator() { let on_controller_change = options.on_controller_change.clone(); let js_closure = Closure::wrap(Box::new(move |_event: JsValue| { - on_controller_change.call(()); + on_controller_change(); }) as Box) .into_js_value(); navigator @@ -95,7 +101,7 @@ pub fn use_service_worker_with_options(options: UseServiceWorkerOptions) -> UseS } Err(err) => match err { ServiceWorkerRegistrationError::Js(err) => { - tracing::warn!("ServiceWorker registration failed: {err:?}") + logging::warn!("ServiceWorker registration failed: {err:?}") } ServiceWorkerRegistrationError::NeverQueried => {} }, @@ -125,28 +131,28 @@ pub fn use_service_worker_with_options(options: UseServiceWorkerOptions) -> UseS .unwrap_or_default() }) }), - check_for_update: Callback::new(move |()| { + check_for_update: move || { registration.with(|reg| { if let Ok(reg) = reg { update_sw.dispatch(reg.clone()) } }) - }), - skip_waiting: Callback::new(move |()| { + }, + skip_waiting: move || { registration.with_untracked(|reg| if let Ok(reg) = reg { match reg.waiting() { Some(sw) => { - tracing::info!("Updating to newly installed SW..."); + logging::debug_warn!("Updating to newly installed SW..."); if let Err(err) = sw.post_message(&JsValue::from_str(&options.skip_waiting_message)) { - tracing::warn!("Could not send message to active SW: Error: {err:?}"); + logging::warn!("Could not send message to active SW: Error: {err:?}"); } }, None => { - tracing::warn!("You tried to update the SW while no new SW was waiting. This is probably a bug."); + logging::warn!("You tried to update the SW while no new SW was waiting. This is probably a bug."); }, } }); - }), + }, } } @@ -155,16 +161,18 @@ pub fn use_service_worker_with_options(options: UseServiceWorkerOptions) -> UseS pub struct UseServiceWorkerOptions { /// The name of your service-worker file. Must be deployed alongside your app. /// The default name is 'service-worker.js'. - pub script_url: Cow<'static, str>, + #[builder(into)] + script_url: String, /// The message sent to a waiting ServiceWorker when you call the `skip_waiting` callback. /// The callback is part of the return type of [`use_service_worker`]! /// The default message is 'skipWaiting'. - pub skip_waiting_message: Cow<'static, str>, + #[builder(into)] + skip_waiting_message: String, /// What should happen when a new service worker was activated? /// The default implementation reloads the current page. - pub on_controller_change: Callback<()>, + on_controller_change: Rc, } impl Default for UseServiceWorkerOptions { @@ -172,11 +180,11 @@ impl Default for UseServiceWorkerOptions { Self { script_url: "service-worker.js".into(), skip_waiting_message: "skipWaiting".into(), - on_controller_change: Callback::new(move |()| { + on_controller_change: Rc::new(move || { use std::ops::Deref; if let Some(window) = use_window().deref() { if let Err(err) = window.location().reload() { - tracing::warn!( + logging::warn!( "Detected a ServiceWorkerController change but the page reload failed! Error: {err:?}" ); } @@ -187,7 +195,11 @@ impl Default for UseServiceWorkerOptions { } /// Return type of [`use_service_worker`]. -pub struct UseServiceWorkerReturn { +pub struct UseServiceWorkerReturn +where + CheckFn: Fn() + Clone, + SkipFn: Fn() + Clone, +{ /// The current registration state. pub registration: Signal>, @@ -201,11 +213,11 @@ pub struct UseServiceWorkerReturn { pub active: Signal, /// Check for a ServiceWorker update. - pub check_for_update: Callback<()>, + pub check_for_update: CheckFn, /// Call this to activate a new ("waiting") SW if one is available. /// Calling this while the [`UseServiceWorkerReturn::waiting`] signal resolves to false has no effect. - pub skip_waiting: Callback<()>, + pub skip_waiting: SkipFn, } struct ServiceWorkerScriptUrl(pub String); From 65f8b89d100610277618154ac092ae13c601b79f Mon Sep 17 00:00:00 2001 From: Maccesch Date: Tue, 24 Oct 2023 00:39:21 -0500 Subject: [PATCH 12/90] made use_service_worker more in line with the other functions --- Cargo.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0031e6d..e80e4fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,17 +13,17 @@ repository = "https://github.com/Synphonyte/leptos-use" homepage = "https://leptos-use.rs" [dependencies] -leptos = "0.5" -wasm-bindgen = "0.2" -wasm-bindgen-futures = "0.4" -js-sys = "0.3" +cfg-if = "1" default-struct-builder = "0.5" +js-sys = "0.3" +lazy_static = "1" +leptos = "0.5" num = { version = "0.4", optional = true } +paste = "1" serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } -paste = "1" -lazy_static = "1" -cfg-if = "1" +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" [dependencies.web-sys] version = "0.3" From 8c2beb814da171ecaf295ae7437d711720a8f75f Mon Sep 17 00:00:00 2001 From: Maccesch Date: Tue, 24 Oct 2023 00:44:23 -0500 Subject: [PATCH 13/90] release 0.8.0 --- .idea/leptos-use.iml | 2 ++ CHANGELOG.md | 6 ++-- Cargo.toml | 2 +- README.md | 12 ++++---- docs/book/src/introduction.md | 2 +- examples/use_web_notification/src/main.rs | 13 +++----- src/lib.rs | 4 +-- src/use_infinite_scroll.rs | 37 +++++++++++------------ src/use_web_notification.rs | 12 ++++---- 9 files changed, 44 insertions(+), 46 deletions(-) diff --git a/.idea/leptos-use.iml b/.idea/leptos-use.iml index bb3717d..830c9eb 100644 --- a/.idea/leptos-use.iml +++ b/.idea/leptos-use.iml @@ -58,6 +58,8 @@ + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 1585ee9..be42cc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,13 +3,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - +## [0.8.0] - 2023-10-24 ### New Functions 🚀 -- `use_web_notification` - @centershocks44 +- `use_web_notification` (thanks to @centershocks44) - `use_infinite_scroll` -- `use_service_worker` - @lpotthast +- `use_service_worker` (thanks to @lpotthast) ### Breaking Changes 🛠 diff --git a/Cargo.toml b/Cargo.toml index 0805adb..9b19c02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos-use" -version = "0.7.2" +version = "0.8.0" edition = "2021" authors = ["Marc-Stefan Cassola"] categories = ["gui", "web-programming"] diff --git a/README.md b/README.md index f398a8d..e2771df 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Crates.io SSR Docs & Demos - 59 Functions + 62 Functions


@@ -87,8 +87,8 @@ This will create the function file in the src directory, scaffold an example dir ## Leptos compatibility -| Crate version | Compatible Leptos version | -|---------------|---------------------------| -| <= 0.3 | 0.3 | -| 0.4, 0.5, 0.6 | 0.4 | -| 0.7 | 0.5 | +| Crate version | Compatible Leptos version | +|----------------|---------------------------| +| <= 0.3 | 0.3 | +| 0.4, 0.5, 0.6 | 0.4 | +| 0.7, 0.8 | 0.5 | diff --git a/docs/book/src/introduction.md b/docs/book/src/introduction.md index 5381e52..d9a1704 100644 --- a/docs/book/src/introduction.md +++ b/docs/book/src/introduction.md @@ -12,6 +12,6 @@ Crates.io SSR Docs & Demos - 59 Functions + 62 Functions

\ No newline at end of file diff --git a/examples/use_web_notification/src/main.rs b/examples/use_web_notification/src/main.rs index b9045bc..bc717bb 100644 --- a/examples/use_web_notification/src/main.rs +++ b/examples/use_web_notification/src/main.rs @@ -24,23 +24,20 @@ fn Demo() -> impl IntoView { view! {
-

- Supported: -

+

Supported:

The Notification Web API is not supported in your browser.
+ fallback=|| { + view! {
The Notification Web API is not supported in your browser.
} } > + + }>Show Notification } } diff --git a/src/lib.rs b/src/lib.rs index 8f48103..a9700c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,6 @@ mod is_none; mod is_ok; mod is_some; mod on_click_outside; -mod use_web_notification; mod signal_debounced; mod signal_throttled; mod use_active_element; @@ -62,6 +61,7 @@ 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; @@ -77,7 +77,6 @@ pub use is_none::*; pub use is_ok::*; pub use is_some::*; pub use on_click_outside::*; -pub use use_web_notification::*; pub use signal_debounced::*; pub use signal_throttled::*; pub use use_active_element::*; @@ -114,6 +113,7 @@ 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_window::*; pub use use_window_focus::*; diff --git a/src/use_infinite_scroll.rs b/src/use_infinite_scroll.rs index 8153c4f..d02d97d 100644 --- a/src/use_infinite_scroll.rs +++ b/src/use_infinite_scroll.rs @@ -150,26 +150,25 @@ where scroll_width <= client_width }; - if state.arrived_state.get_untracked().get_direction(direction) || is_narrower { - if !is_loading.get_untracked() { - set_loading.set(true); + if (state.arrived_state.get_untracked().get_direction(direction) || is_narrower) + && !is_loading.get_untracked() + { + set_loading.set(true); - let state = state.clone(); - let measure = measure.clone(); - spawn_local(async move { - join!( - on_load_more.with_value(|f| f(state)), - sleep(Duration::from_millis(interval as u64)) - ); + 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(); - } - }); - } + set_loading.set(false); + sleep(Duration::ZERO).await; + measure(); + if let Some(check_and_load) = check_and_load.get_value() { + check_and_load(); + } + }); } } } @@ -178,7 +177,7 @@ where let _ = watch( move || is_element_visible.get(), move |visible, prev_visible, _| { - if *visible && !prev_visible.map(|v| *v).unwrap_or_default() { + if *visible && !prev_visible.copied().unwrap_or_default() { measure(); } }, diff --git a/src/use_web_notification.rs b/src/use_web_notification.rs index 92a7024..98c66ef 100644 --- a/src/use_web_notification.rs +++ b/src/use_web_notification.rs @@ -140,7 +140,7 @@ pub fn use_web_notification_with_options( set_permission.set(request_web_notification_permission().await); }); - on_cleanup(close.clone()); + on_cleanup(close); // Use close() to remove a notification that is no longer relevant to to // the user (e.g.the user already read the notification on the webpage). @@ -157,7 +157,7 @@ pub fn use_web_notification_with_options( } UseWebNotificationReturn { - is_supported: is_supported.into(), + is_supported, notification: notification.into(), show, close, @@ -274,19 +274,19 @@ impl From<&UseWebNotificationOptions> for web_sys::NotificationOptions { // .renotify(options.renotify); if let Some(body) = &options.body { - web_sys_options.body(&body); + web_sys_options.body(body); } if let Some(icon) = &options.icon { - web_sys_options.icon(&icon); + web_sys_options.icon(icon); } if let Some(language) = &options.language { - web_sys_options.lang(&language); + web_sys_options.lang(language); } if let Some(tag) = &options.tag { - web_sys_options.tag(&tag); + web_sys_options.tag(tag); } web_sys_options From fcda13de8d8473156032622c847c4c5590d96387 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Thu, 26 Oct 2023 11:44:59 +0100 Subject: [PATCH 14/90] Add thiserror dependency --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 9b19c02..60f1b5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ num = { version = "0.4", optional = true } paste = "1" serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } +thiserror = "1.0" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" From 2f3c72f2bb17d419e4368fc8a7509cb4bf810f01 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Thu, 26 Oct 2023 11:46:47 +0100 Subject: [PATCH 15/90] Prototype use_storage replacement that uses TryFrom --- src/lib.rs | 2 + src/use_storage.rs | 165 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 src/use_storage.rs diff --git a/src/lib.rs b/src/lib.rs index a9700c1..c16506d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,7 @@ mod use_raf_fn; mod use_scroll; mod use_service_worker; mod use_sorted; +mod use_storage; mod use_supported; mod use_throttle_fn; mod use_timestamp; @@ -109,6 +110,7 @@ pub use use_raf_fn::*; pub use use_scroll::*; pub use use_service_worker::*; pub use use_sorted::*; +pub use use_storage::*; pub use use_supported::*; pub use use_throttle_fn::*; pub use use_timestamp::*; diff --git a/src/use_storage.rs b/src/use_storage.rs new file mode 100644 index 0000000..b5b8edb --- /dev/null +++ b/src/use_storage.rs @@ -0,0 +1,165 @@ +use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions}; +use leptos::*; +use std::rc::Rc; +use thiserror::Error; +use wasm_bindgen::JsValue; +use web_sys::Storage; + +#[derive(Clone)] +pub struct UseStorageOptions { + on_error: Rc)>, +} + +/// Session handling errors returned by [`use_storage`]. +#[derive(Error, Debug)] +pub enum UseStorageError { + #[error("window not available")] + WindowReturnedNone, + #[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 parse item value")] + ParseItemError(Err), +} + +/// Hook for using local storage. Returns a result of a signal and a setter / deleter. +pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + PartialEq + TryFrom + ToString, + T::Error: std::fmt::Debug, +{ + use_local_storage_with_options(key, UseStorageOptions::default()) +} + +/// Hook for using local storage. Returns a result of a signal and a setter / deleter. +pub fn use_local_storage_with_options( + key: impl AsRef, + options: UseStorageOptions, +) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + PartialEq + TryFrom + ToString, +{ + // TODO ssr + let UseStorageOptions { on_error } = options; + let storage: Result = handle_error(&on_error, try_storage()); + + let initial_value = storage + .to_owned() + // Get initial item from storage + .and_then(|s| { + let result = s + .get_item(key.as_ref()) + .map_err(UseStorageError::GetItemFailed); + handle_error(&on_error, result) + }) + .unwrap_or_default(); + // Attempt to parse the item string + let initial_value = parse_item(initial_value, &on_error); + let (data, set_data) = create_signal(initial_value); + + // Update storage value + let set_value = { + let storage = storage.to_owned(); + let key = key.as_ref().to_owned(); + let on_error = on_error.to_owned(); + move |value: Option| { + let key = key.as_str(); + // Attempt to update storage + let _ = storage.as_ref().map(|storage| { + let result = match value { + // Update + Some(ref value) => storage + .set_item(key, &value.to_string()) + .map_err(UseStorageError::SetItemFailed), + // Remove + None => storage + .remove_item(key) + .map_err(UseStorageError::RemoveItemFailed), + }; + handle_error(&on_error, result) + }); + + // Notify signal of change + set_data.set(value); + } + }; + + // Listen for storage events + // Note: we only receive events from other tabs / windows, not from internal updates. + let _ = { + let key = key.as_ref().to_owned(); + use_event_listener_with_options( + use_window(), + leptos::ev::storage, + move |ev| { + // Update storage value if our key matches + if let Some(k) = ev.key() { + if k == key { + let value = parse_item(ev.new_value(), &on_error); + set_data.set(value) + } + } else { + // All keys deleted + set_data.set(None) + } + }, + UseEventListenerOptions::default().passive(true), + ) + }; + + let value = create_memo(move |_| data.get().unwrap_or_default()); + (value, set_value) +} + +fn try_storage() -> Result> { + use_window() + .as_ref() + .ok_or_else(|| UseStorageError::WindowReturnedNone)? + .local_storage() + .map_err(|err| UseStorageError::StorageNotAvailable(err))? + .ok_or_else(|| UseStorageError::StorageReturnedNone) +} + +/// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. +fn handle_error( + on_error: &Rc)>, + result: Result>, +) -> Result { + result.or_else(|err| Err((on_error)(err))) +} + +fn parse_item>( + str: Option, + on_error: &Rc)>, +) -> Option { + str.map(|str| { + let result = T::try_from(str).map_err(UseStorageError::ParseItemError); + handle_error(&on_error, result) + }) + .transpose() + // We've sent our error so unwrap to drop () error + .unwrap_or_default() +} + +impl Default for UseStorageOptions { + fn default() -> Self { + Self { + on_error: Rc::new(|_err| ()), + } + } +} + +impl UseStorageOptions { + pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { + Self { + on_error: Rc::new(on_error), + } + } +} From 0f6b4aadc6f71fdc8f791f050919d0c8825815ba Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Thu, 26 Oct 2023 12:01:04 +0100 Subject: [PATCH 16/90] Add prototype use_storage option to listen to changes --- src/use_storage.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/use_storage.rs b/src/use_storage.rs index b5b8edb..80ea6af 100644 --- a/src/use_storage.rs +++ b/src/use_storage.rs @@ -8,6 +8,7 @@ use web_sys::Storage; #[derive(Clone)] pub struct UseStorageOptions { on_error: Rc)>, + listen_to_storage_changes: bool, } /// Session handling errors returned by [`use_storage`]. @@ -47,7 +48,10 @@ where T: Clone + Default + PartialEq + TryFrom + ToString, { // TODO ssr - let UseStorageOptions { on_error } = options; + let UseStorageOptions { + on_error, + listen_to_storage_changes, + } = options; let storage: Result = handle_error(&on_error, try_storage()); let initial_value = storage @@ -93,9 +97,9 @@ where // Listen for storage events // Note: we only receive events from other tabs / windows, not from internal updates. - let _ = { + if listen_to_storage_changes { let key = key.as_ref().to_owned(); - use_event_listener_with_options( + let _ = use_event_listener_with_options( use_window(), leptos::ev::storage, move |ev| { @@ -111,7 +115,7 @@ where } }, UseEventListenerOptions::default().passive(true), - ) + ); }; let value = create_memo(move |_| data.get().unwrap_or_default()); @@ -152,6 +156,7 @@ impl Default for UseStorageOptions { fn default() -> Self { Self { on_error: Rc::new(|_err| ()), + listen_to_storage_changes: true, } } } @@ -160,6 +165,14 @@ impl UseStorageOptions { pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { Self { on_error: Rc::new(on_error), + ..self + } + } + + pub fn listen_to_storage_changes(self, listen_to_storage_changes: bool) -> Self { + Self { + listen_to_storage_changes, + ..self } } } From c6d9ea28e575808782fd57532ea42b48a1b4b9e8 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 10:05:02 +0100 Subject: [PATCH 17/90] Add codec (String, prost, and serde) to use_storage --- Cargo.toml | 5 +- src/lib.rs | 1 + src/use_storage.rs | 185 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 165 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 60f1b5d..a694929 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ repository = "https://github.com/Synphonyte/leptos-use" homepage = "https://leptos-use.rs" [dependencies] +base64 = { version = "0.21", optional = true } cfg-if = "1" default-struct-builder = "0.5" futures-util = "0.3" @@ -22,6 +23,7 @@ lazy_static = "1" leptos = "0.5" num = { version = "0.4", optional = true } paste = "1" +prost = { version = "0.11", optional = true } serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } thiserror = "1.0" @@ -93,11 +95,10 @@ features = [ [features] docs = [] math = ["num"] -storage = ["serde", "serde_json", "web-sys/StorageEvent"] +storage = ["base64", "serde", "serde_json", "web-sys/StorageEvent"] ssr = [] [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg=web_sys_unstable_apis"] rustc-args = ["--cfg=web_sys_unstable_apis"] - diff --git a/src/lib.rs b/src/lib.rs index c16506d..0501fd2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,7 @@ mod use_raf_fn; mod use_scroll; mod use_service_worker; mod use_sorted; +#[cfg(feature = "storage")] mod use_storage; mod use_supported; mod use_throttle_fn; diff --git a/src/use_storage.rs b/src/use_storage.rs index 80ea6af..b2d71bd 100644 --- a/src/use_storage.rs +++ b/src/use_storage.rs @@ -1,13 +1,14 @@ use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions}; use leptos::*; -use std::rc::Rc; +use std::{rc::Rc, str::FromStr}; use thiserror::Error; use wasm_bindgen::JsValue; use web_sys::Storage; #[derive(Clone)] -pub struct UseStorageOptions { - on_error: Rc)>, +pub struct UseStorageOptions> { + codec: C, + on_error: Rc)>, listen_to_storage_changes: bool, } @@ -26,29 +27,30 @@ pub enum UseStorageError { SetItemFailed(JsValue), #[error("failed to delete item")] RemoveItemFailed(JsValue), - #[error("failed to parse item value")] - ParseItemError(Err), + #[error("failed to encode / decode item value")] + ItemCodecError(Err), } /// Hook for using local storage. Returns a result of a signal and a setter / deleter. pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> ()) where - T: Clone + Default + PartialEq + TryFrom + ToString, - T::Error: std::fmt::Debug, + T: Clone + Default + FromStr + PartialEq + ToString, { - use_local_storage_with_options(key, UseStorageOptions::default()) + use_local_storage_with_options(key, UseStorageOptions::string_codec()) } /// Hook for using local storage. Returns a result of a signal and a setter / deleter. -pub fn use_local_storage_with_options( +pub fn use_local_storage_with_options( key: impl AsRef, - options: UseStorageOptions, + options: UseStorageOptions, ) -> (Memo, impl Fn(Option) -> ()) where - T: Clone + Default + PartialEq + TryFrom + ToString, + T: Clone + Default + PartialEq, + C: Codec, { // TODO ssr let UseStorageOptions { + codec, on_error, listen_to_storage_changes, } = options; @@ -65,13 +67,14 @@ where }) .unwrap_or_default(); // Attempt to parse the item string - let initial_value = parse_item(initial_value, &on_error); + let initial_value = decode_item(&codec, initial_value, &on_error); let (data, set_data) = create_signal(initial_value); // Update storage value let set_value = { let storage = storage.to_owned(); let key = key.as_ref().to_owned(); + let codec = codec.to_owned(); let on_error = on_error.to_owned(); move |value: Option| { let key = key.as_str(); @@ -79,9 +82,14 @@ where let _ = storage.as_ref().map(|storage| { let result = match value { // Update - Some(ref value) => storage - .set_item(key, &value.to_string()) - .map_err(UseStorageError::SetItemFailed), + Some(ref value) => codec + .encode(&value) + .map_err(UseStorageError::ItemCodecError) + .and_then(|enc_value| { + storage + .set_item(key, &enc_value) + .map_err(UseStorageError::SetItemFailed) + }), // Remove None => storage .remove_item(key) @@ -106,7 +114,7 @@ where // Update storage value if our key matches if let Some(k) = ev.key() { if k == key { - let value = parse_item(ev.new_value(), &on_error); + let value = decode_item(&codec, ev.new_value(), &on_error); set_data.set(value) } } else { @@ -139,12 +147,13 @@ fn handle_error( result.or_else(|err| Err((on_error)(err))) } -fn parse_item>( +fn decode_item>( + codec: &C, str: Option, - on_error: &Rc)>, + on_error: &Rc)>, ) -> Option { str.map(|str| { - let result = T::try_from(str).map_err(UseStorageError::ParseItemError); + let result = codec.decode(str).map_err(UseStorageError::ItemCodecError); handle_error(&on_error, result) }) .transpose() @@ -152,17 +161,16 @@ fn parse_item>( .unwrap_or_default() } -impl Default for UseStorageOptions { - fn default() -> Self { +impl> UseStorageOptions { + fn new(codec: C) -> Self { Self { + codec, on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, } } -} -impl UseStorageOptions { - pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { + pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { Self { on_error: Rc::new(on_error), ..self @@ -176,3 +184,132 @@ impl UseStorageOptions { } } } + +pub trait Codec: Clone + 'static { + type Error; + fn encode(&self, val: &T) -> Result; + fn decode(&self, str: String) -> Result; +} + +#[derive(Clone, 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) + } +} + +impl UseStorageOptions { + pub fn string_codec() -> Self { + Self::new(StringCodec()) + } +} + +#[derive(Clone, 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), +} + +use base64::Engine; +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) + } +} + +impl UseStorageOptions { + pub fn prost_codec() -> Self { + Self::new(ProstCodec()) + } +} + +#[derive(Clone, 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) + } +} + +impl UseStorageOptions { + pub fn json_codec() -> Self { + Self::new(JsonCodec()) + } +} + +#[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)); + } + + #[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)); + } + + #[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); + } +} From 0fb4f7e6a1e9e26d106df7538f25df2fda8f8a6e Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 11:56:47 +0100 Subject: [PATCH 18/90] Problem: only local storage is available. Provide session and generic web_sys::Storage fns --- src/use_storage.rs | 96 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 15 deletions(-) diff --git a/src/use_storage.rs b/src/use_storage.rs index b2d71bd..7988d13 100644 --- a/src/use_storage.rs +++ b/src/use_storage.rs @@ -3,7 +3,7 @@ use leptos::*; use std::{rc::Rc, str::FromStr}; use thiserror::Error; use wasm_bindgen::JsValue; -use web_sys::Storage; +use web_sys::{Storage, Window}; #[derive(Clone)] pub struct UseStorageOptions> { @@ -36,11 +36,74 @@ pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option where T: Clone + Default + FromStr + PartialEq + ToString, { - use_local_storage_with_options(key, UseStorageOptions::string_codec()) + use_storage_with_options(get_local_storage, key, UseStorageOptions::string_codec()) +} + +pub fn use_local_storage_with_options( + key: impl AsRef, + options: UseStorageOptions, +) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + PartialEq, + C: Codec, +{ + use_storage_with_options(|w| w.local_storage(), key, options) +} + +fn get_local_storage(w: &Window) -> Result, JsValue> { + w.local_storage() +} + +/// Hook for using session storage. Returns a result of a signal and a setter / deleter. +pub fn use_session_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + FromStr + PartialEq + ToString, +{ + use_storage_with_options(get_session_storage, key, UseStorageOptions::string_codec()) +} + +pub fn use_session_storage_with_options( + key: impl AsRef, + options: UseStorageOptions, +) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + PartialEq, + C: Codec, +{ + use_storage_with_options(get_session_storage, key, options) +} + +fn get_session_storage(w: &Window) -> Result, JsValue> { + w.session_storage() +} + +/// Hook for using custom storage. Returns a result of a signal and a setter / deleter. +pub fn use_custom_storage( + storage: impl Into, + key: impl AsRef, +) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + FromStr + PartialEq + ToString, +{ + use_custom_storage_with_options(storage, key, UseStorageOptions::string_codec()) +} + +pub fn use_custom_storage_with_options( + storage: impl Into, + key: impl AsRef, + options: UseStorageOptions, +) -> (Memo, impl Fn(Option) -> ()) +where + T: Clone + Default + PartialEq, + C: Codec, +{ + let storage = storage.into(); + use_storage_with_options(|_| Ok(Some(storage)), key, options) } /// Hook for using local storage. Returns a result of a signal and a setter / deleter. -pub fn use_local_storage_with_options( +fn use_storage_with_options( + get_storage: impl FnOnce(&Window) -> Result, JsValue>, key: impl AsRef, options: UseStorageOptions, ) -> (Memo, impl Fn(Option) -> ()) @@ -54,11 +117,22 @@ where on_error, listen_to_storage_changes, } = options; - let storage: Result = handle_error(&on_error, try_storage()); + // Get storage API + let storage = use_window() + .as_ref() + .ok_or(UseStorageError::WindowReturnedNone) + .and_then(|w| { + get_storage(w) + .map_err(UseStorageError::StorageNotAvailable) + .and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone)) + }); + let storage: Result = handle_error(&on_error, storage); + + // Fetch initial value (undecoded) let initial_value = storage .to_owned() - // Get initial item from storage + // Pull from storage .and_then(|s| { let result = s .get_item(key.as_ref()) @@ -66,8 +140,9 @@ where handle_error(&on_error, result) }) .unwrap_or_default(); - // Attempt to parse the item string + // Decode initial value let initial_value = decode_item(&codec, initial_value, &on_error); + let (data, set_data) = create_signal(initial_value); // Update storage value @@ -130,15 +205,6 @@ where (value, set_value) } -fn try_storage() -> Result> { - use_window() - .as_ref() - .ok_or_else(|| UseStorageError::WindowReturnedNone)? - .local_storage() - .map_err(|err| UseStorageError::StorageNotAvailable(err))? - .ok_or_else(|| UseStorageError::StorageReturnedNone) -} - /// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. fn handle_error( on_error: &Rc)>, From 4538097ab130543637d0d2b831cc2646f9739fb1 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 12:37:41 +0100 Subject: [PATCH 19/90] Problem: use_color_mode relies on specifying backing via StorageType. Expose use_storage_with_options and use StorageType --- src/use_storage.rs | 65 ++++++++++------------------------------------ 1 file changed, 14 insertions(+), 51 deletions(-) diff --git a/src/use_storage.rs b/src/use_storage.rs index 7988d13..cac53bb 100644 --- a/src/use_storage.rs +++ b/src/use_storage.rs @@ -1,9 +1,10 @@ -use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions}; +use crate::{ + core::StorageType, use_event_listener_with_options, use_window, UseEventListenerOptions, +}; use leptos::*; use std::{rc::Rc, str::FromStr}; use thiserror::Error; use wasm_bindgen::JsValue; -use web_sys::{Storage, Window}; #[derive(Clone)] pub struct UseStorageOptions> { @@ -15,8 +16,6 @@ pub struct UseStorageOptions> { /// Session handling errors returned by [`use_storage`]. #[derive(Error, Debug)] pub enum UseStorageError { - #[error("window not available")] - WindowReturnedNone, #[error("storage not available")] StorageNotAvailable(JsValue), #[error("storage not returned from window")] @@ -36,7 +35,7 @@ pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option where T: Clone + Default + FromStr + PartialEq + ToString, { - use_storage_with_options(get_local_storage, key, UseStorageOptions::string_codec()) + use_storage_with_options(StorageType::Local, key, UseStorageOptions::string_codec()) } pub fn use_local_storage_with_options( @@ -47,11 +46,7 @@ where T: Clone + Default + PartialEq, C: Codec, { - use_storage_with_options(|w| w.local_storage(), key, options) -} - -fn get_local_storage(w: &Window) -> Result, JsValue> { - w.local_storage() + use_storage_with_options(StorageType::Local, key, options) } /// Hook for using session storage. Returns a result of a signal and a setter / deleter. @@ -59,7 +54,7 @@ pub fn use_session_storage(key: impl AsRef) -> (Memo, impl Fn(Option< where T: Clone + Default + FromStr + PartialEq + ToString, { - use_storage_with_options(get_session_storage, key, UseStorageOptions::string_codec()) + use_storage_with_options(StorageType::Session, key, UseStorageOptions::string_codec()) } pub fn use_session_storage_with_options( @@ -70,40 +65,12 @@ where T: Clone + Default + PartialEq, C: Codec, { - use_storage_with_options(get_session_storage, key, options) + use_storage_with_options(StorageType::Session, key, options) } -fn get_session_storage(w: &Window) -> Result, JsValue> { - w.session_storage() -} - -/// Hook for using custom storage. Returns a result of a signal and a setter / deleter. -pub fn use_custom_storage( - storage: impl Into, - key: impl AsRef, -) -> (Memo, impl Fn(Option) -> ()) -where - T: Clone + Default + FromStr + PartialEq + ToString, -{ - use_custom_storage_with_options(storage, key, UseStorageOptions::string_codec()) -} - -pub fn use_custom_storage_with_options( - storage: impl Into, - key: impl AsRef, - options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> ()) -where - T: Clone + Default + PartialEq, - C: Codec, -{ - let storage = storage.into(); - use_storage_with_options(|_| Ok(Some(storage)), key, options) -} - -/// Hook for using local storage. Returns a result of a signal and a setter / deleter. +/// Hook for using any kind of storage. Returns a result of a signal and a setter / deleter. fn use_storage_with_options( - get_storage: impl FnOnce(&Window) -> Result, JsValue>, + storage_type: StorageType, key: impl AsRef, options: UseStorageOptions, ) -> (Memo, impl Fn(Option) -> ()) @@ -119,15 +86,11 @@ where } = options; // Get storage API - let storage = use_window() - .as_ref() - .ok_or(UseStorageError::WindowReturnedNone) - .and_then(|w| { - get_storage(w) - .map_err(UseStorageError::StorageNotAvailable) - .and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone)) - }); - let storage: Result = handle_error(&on_error, storage); + 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); // Fetch initial value (undecoded) let initial_value = storage From bdeadba508eb9e035e9f9e61e0e2d255e492e2ce Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 12:39:58 +0100 Subject: [PATCH 20/90] Delete existing storage/ in favour of use_storage.rs --- src/storage/mod.rs | 8 - src/storage/shared.rs | 96 ------ src/storage/use_local_storage.rs | 22 -- src/storage/use_session_storage.rs | 22 -- src/storage/use_storage.rs | 498 ----------------------------- 5 files changed, 646 deletions(-) delete mode 100644 src/storage/mod.rs delete mode 100644 src/storage/shared.rs delete mode 100644 src/storage/use_local_storage.rs delete mode 100644 src/storage/use_session_storage.rs delete mode 100644 src/storage/use_storage.rs diff --git a/src/storage/mod.rs b/src/storage/mod.rs deleted file mode 100644 index bc0844b..0000000 --- a/src/storage/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod shared; -mod use_local_storage; -mod use_session_storage; -mod use_storage; - -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 deleted file mode 100644 index 3ed7716..0000000 --- a/src/storage/use_local_storage.rs +++ /dev/null @@ -1,22 +0,0 @@ -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_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`] -); diff --git a/src/storage/use_session_storage.rs b/src/storage/use_session_storage.rs deleted file mode 100644 index dca9b95..0000000 --- a/src/storage/use_session_storage.rs +++ /dev/null @@ -1,22 +0,0 @@ -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_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`] -); diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs deleted file mode 100644 index 7122882..0000000 --- a/src/storage/use_storage.rs +++ /dev/null @@ -1,498 +0,0 @@ -#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports, dead_code))] - -use crate::core::MaybeRwSignal; -use crate::utils::FilterOptions; -use crate::{ - filter_builder_methods, use_event_listener, watch_pausable_with_options, DebounceOptions, - ThrottleOptions, WatchOptions, WatchPausableReturn, -}; -use cfg_if::cfg_if; -use default_struct_builder::DefaultBuilder; -use js_sys::Reflect; -use leptos::*; -use serde::{Deserialize, Serialize}; -use serde_json::Error; -use std::rc::Rc; -use std::time::Duration; -use wasm_bindgen::{JsCast, JsValue}; - -pub use crate::core::StorageType; - -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). -/// -/// ## Demo -/// -/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_storage) -/// -/// ## Usage -/// -/// It returns a triplet `(read_signal, write_signal, delete_from_storage_func)` of type `(ReadSignal, WriteSignal, Fn())`. -/// -/// Values are (de-)serialized to/from JSON using [`serde`](https://serde.rs/). -/// -/// ``` -/// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_storage, use_storage_with_options, UseStorageOptions}; -/// # 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 -/// -/// // bind bool. -/// let (flag, set_flag, remove_flag) = use_storage("my-flag", true); // returns Signal -/// -/// // bind number -/// let (count, set_count, _) = use_storage("my-count", 0); // returns Signal -/// -/// // bind string with SessionStorage -/// let (id, set_id, _) = use_storage_with_options( -/// "my-id", -/// "some_string_id".to_string(), -/// UseStorageOptions::default().storage_type(StorageType::Session), -/// ); -/// # 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)] -/// 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)) -/// } -/// }), -/// ); -/// # -/// # 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()) -} - -/// 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, -) -> (Signal, WriteSignal, impl Fn() + Clone) -where - for<'de> T: Serialize + Deserialize<'de> + Clone + 'static, - D: Into>, - T: Clone, -{ - let defaults = defaults.into(); - - let UseStorageOptions { - storage_type, - listen_to_storage_changes, - write_defaults, - merge_defaults, - on_error, - filter, - } = options; - - let (data, set_data) = defaults.into_signal(); - - let raw_init = data.get_untracked(); - - cfg_if! { if #[cfg(feature = "ssr")] { - let remove: Rc = Rc::new(|| {}); - } else { - let storage = storage_type.into_storage(); - - 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(); - - 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); - }) - } - Err(e) => { - on_error(UseStorageError::NoStorage(e)); - Rc::new(move || {}) - } - _ => { - // do nothing - Rc::new(move || {}) - } - }; - }} - - (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, -} - -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(), - } - } -} - -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(), - } - } -} - -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() - } -} - -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 { - fn default() -> Self { - Self { - storage_type: Default::default(), - 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 UseStorageOptions { - filter_builder_methods!( - /// the serializing and storing into storage - filter - ); -} From f3c87a1c50777aeed06bedc2698e7d4f3106a024 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 12:41:42 +0100 Subject: [PATCH 21/90] Move use_storage.rs under storage/ --- src/lib.rs | 4 ---- src/storage/mod.rs | 4 ++++ src/{ => storage}/use_storage.rs | 0 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 src/storage/mod.rs rename src/{ => storage}/use_storage.rs (100%) diff --git a/src/lib.rs b/src/lib.rs index 0501fd2..7ca96a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,6 @@ pub mod core; pub mod docs; #[cfg(feature = "math")] pub mod math; -#[cfg(feature = "storage")] pub mod storage; pub mod utils; @@ -57,8 +56,6 @@ mod use_raf_fn; mod use_scroll; mod use_service_worker; mod use_sorted; -#[cfg(feature = "storage")] -mod use_storage; mod use_supported; mod use_throttle_fn; mod use_timestamp; @@ -111,7 +108,6 @@ pub use use_raf_fn::*; pub use use_scroll::*; pub use use_service_worker::*; pub use use_sorted::*; -pub use use_storage::*; pub use use_supported::*; pub use use_throttle_fn::*; pub use use_timestamp::*; diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..cfb8520 --- /dev/null +++ b/src/storage/mod.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "storage")] +mod use_storage; + +pub use use_storage::*; diff --git a/src/use_storage.rs b/src/storage/use_storage.rs similarity index 100% rename from src/use_storage.rs rename to src/storage/use_storage.rs From fdb9f672d1c28d10c0b566434f3828af67b23215 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 13:57:36 +0100 Subject: [PATCH 22/90] Apply use_signal default value via an optional signal --- src/storage/use_storage.rs | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index cac53bb..38b1dd8 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -1,5 +1,6 @@ use crate::{ - core::StorageType, use_event_listener_with_options, use_window, UseEventListenerOptions, + core::{MaybeRwSignal, StorageType}, + use_event_listener_with_options, use_window, UseEventListenerOptions, }; use leptos::*; use std::{rc::Rc, str::FromStr}; @@ -7,10 +8,11 @@ use thiserror::Error; use wasm_bindgen::JsValue; #[derive(Clone)] -pub struct UseStorageOptions> { +pub struct UseStorageOptions> { codec: C, on_error: Rc)>, listen_to_storage_changes: bool, + default_value: MaybeSignal, } /// Session handling errors returned by [`use_storage`]. @@ -43,7 +45,7 @@ pub fn use_local_storage_with_options( options: UseStorageOptions, ) -> (Memo, impl Fn(Option) -> ()) where - T: Clone + Default + PartialEq, + T: Clone + PartialEq, C: Codec, { use_storage_with_options(StorageType::Local, key, options) @@ -62,7 +64,7 @@ pub fn use_session_storage_with_options( options: UseStorageOptions, ) -> (Memo, impl Fn(Option) -> ()) where - T: Clone + Default + PartialEq, + T: Clone + PartialEq, C: Codec, { use_storage_with_options(StorageType::Session, key, options) @@ -75,7 +77,7 @@ fn use_storage_with_options( options: UseStorageOptions, ) -> (Memo, impl Fn(Option) -> ()) where - T: Clone + Default + PartialEq, + T: Clone + PartialEq, C: Codec, { // TODO ssr @@ -83,6 +85,7 @@ where codec, on_error, listen_to_storage_changes, + default_value, } = options; // Get storage API @@ -164,7 +167,9 @@ where ); }; - let value = create_memo(move |_| data.get().unwrap_or_default()); + // Apply default value + let value = create_memo(move |_| data.get().unwrap_or_else(|| default_value.get())); + (value, set_value) } @@ -190,12 +195,13 @@ fn decode_item>( .unwrap_or_default() } -impl> UseStorageOptions { +impl> UseStorageOptions { fn new(codec: C) -> Self { Self { codec, on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, + default_value: MaybeSignal::default(), } } @@ -212,6 +218,13 @@ impl> UseStorageOptions { ..self } } + + pub fn default_value(self, values: impl Into>) -> Self { + Self { + default_value: values.into().into_signal().0.into(), + ..self + } + } } pub trait Codec: Clone + 'static { @@ -235,7 +248,7 @@ impl Codec for StringCodec { } } -impl UseStorageOptions { +impl UseStorageOptions { pub fn string_codec() -> Self { Self::new(StringCodec()) } @@ -269,7 +282,7 @@ impl Codec for ProstCodec { } } -impl UseStorageOptions { +impl UseStorageOptions { pub fn prost_codec() -> Self { Self::new(ProstCodec()) } @@ -290,7 +303,9 @@ impl Codec for JsonCodec { } } -impl UseStorageOptions { +impl + UseStorageOptions +{ pub fn json_codec() -> Self { Self::new(JsonCodec()) } From 7bfd0690476a06f2e946e70550abadccce611a58 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 14:15:40 +0100 Subject: [PATCH 23/90] Expose generalised use of use_storage_with_options --- src/storage/use_storage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 38b1dd8..1f4e992 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -71,7 +71,7 @@ where } /// Hook for using any kind of storage. Returns a result of a signal and a setter / deleter. -fn use_storage_with_options( +pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, options: UseStorageOptions, From 7b5456a4618f93c93b971880f52ac0f5fdd00d00 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 14:27:53 +0100 Subject: [PATCH 24/90] Remove storage feature --- Cargo.toml | 2 +- examples/ssr/Cargo.toml | 2 +- examples/use_color_mode/Cargo.toml | 2 +- examples/use_storage/Cargo.toml | 2 +- src/core/storage.rs | 1 - src/storage/mod.rs | 1 - src/use_color_mode.rs | 68 ++++++++++-------------------- 7 files changed, 26 insertions(+), 52 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a694929..8405ad8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ features = [ "ServiceWorkerRegistration", "ServiceWorkerState", "Storage", + "StorageEvent", "Touch", "TouchEvent", "TouchList", @@ -95,7 +96,6 @@ features = [ [features] docs = [] math = ["num"] -storage = ["base64", "serde", "serde_json", "web-sys/StorageEvent"] ssr = [] [package.metadata.docs.rs] diff --git a/examples/ssr/Cargo.toml b/examples/ssr/Cargo.toml index 5010f37..782e066 100644 --- a/examples/ssr/Cargo.toml +++ b/examples/ssr/Cargo.toml @@ -15,7 +15,7 @@ leptos = { version = "0.5", features = ["nightly"] } leptos_axum = { version = "0.5", optional = true } leptos_meta = { version = "0.5", features = ["nightly"] } leptos_router = { version = "0.5", features = ["nightly"] } -leptos-use = { path = "../..", features = ["storage"] } +leptos-use = { path = "../.." } log = "0.4" simple_logger = "4" tokio = { version = "1.25.0", optional = true } diff --git a/examples/use_color_mode/Cargo.toml b/examples/use_color_mode/Cargo.toml index 308c9c2..0b58b63 100644 --- a/examples/use_color_mode/Cargo.toml +++ b/examples/use_color_mode/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"] } web-sys = "0.3" [dev-dependencies] 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/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/storage/mod.rs b/src/storage/mod.rs index cfb8520..2320414 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "storage")] mod use_storage; pub use use_storage::*; diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index 3bffcb5..86efba4 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -1,13 +1,9 @@ use crate::core::{ElementMaybeSignal, MaybeRwSignal}; -#[cfg(feature = "storage")] use crate::storage::{use_storage_with_options, UseStorageOptions}; -#[cfg(feature = "storage")] -use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; 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; @@ -255,49 +251,29 @@ 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 { + use_storage_with_options( + storage_key, + initial_value, + UseStorageOptions::default() + .listen_to_storage_changes(listen_to_storage_changes) + .storage_type(storage), + ) + } 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 { From 861633dd1e84accd462cb915f3d5bc8b6a13df5f Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 14:32:03 +0100 Subject: [PATCH 25/90] Gate prost and serde dependencies behind features --- Cargo.toml | 2 + src/storage/codec_json.rs | 46 +++++++++++++++++++ src/storage/codec_prost.rs | 58 ++++++++++++++++++++++++ src/storage/mod.rs | 4 ++ src/storage/use_storage.rs | 93 +------------------------------------- 5 files changed, 111 insertions(+), 92 deletions(-) create mode 100644 src/storage/codec_json.rs create mode 100644 src/storage/codec_prost.rs diff --git a/Cargo.toml b/Cargo.toml index 8405ad8..4de6bbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,8 @@ features = [ [features] docs = [] math = ["num"] +prost = ["base64", "dep:prost"] +serde = ["dep:serde", "serde_json"] ssr = [] [package.metadata.docs.rs] diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs new file mode 100644 index 0000000..fc065a7 --- /dev/null +++ b/src/storage/codec_json.rs @@ -0,0 +1,46 @@ +use super::{Codec, UseStorageOptions}; + +#[derive(Clone, 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) + } +} + +impl + UseStorageOptions +{ + pub fn json_codec() -> Self { + Self::new(JsonCodec()) + } +} + +#[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..0b0bfa7 --- /dev/null +++ b/src/storage/codec_prost.rs @@ -0,0 +1,58 @@ +use super::{Codec, UseStorageOptions}; +use base64::Engine; +use thiserror::Error; + +#[derive(Clone, 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) + } +} + +impl UseStorageOptions { + pub fn prost_codec() -> Self { + Self::new(ProstCodec()) + } +} + +#[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/mod.rs b/src/storage/mod.rs index 2320414..eeae93e 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,3 +1,7 @@ +#[cfg(feature = "serde")] +mod codec_json; +#[cfg(feature = "prost")] +mod codec_prost; mod use_storage; pub use use_storage::*; diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 1f4e992..c296805 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -196,7 +196,7 @@ fn decode_item>( } impl> UseStorageOptions { - fn new(codec: C) -> Self { + pub(super) fn new(codec: C) -> Self { Self { codec, on_error: Rc::new(|_err| ()), @@ -254,63 +254,6 @@ impl UseStorageOptions } } -#[derive(Clone, 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), -} - -use base64::Engine; -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) - } -} - -impl UseStorageOptions { - pub fn prost_codec() -> Self { - Self::new(ProstCodec()) - } -} - -#[derive(Clone, 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) - } -} - -impl - UseStorageOptions -{ - pub fn json_codec() -> Self { - Self::new(JsonCodec()) - } -} - #[cfg(test)] mod tests { use super::*; @@ -322,38 +265,4 @@ mod tests { assert_eq!(codec.encode(&s), Ok(s.clone())); assert_eq!(codec.decode(s.clone()), Ok(s)); } - - #[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)); - } - - #[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); - } } From bbebd8a67f036dfd9cd7d5a2628caafa278f4e0c Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 14:50:29 +0100 Subject: [PATCH 26/90] use_storage SSR should return signal with defaults --- src/storage/use_storage.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index c296805..d048b9c 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -2,6 +2,7 @@ use crate::{ core::{MaybeRwSignal, StorageType}, use_event_listener_with_options, use_window, UseEventListenerOptions, }; +use cfg_if::cfg_if; use leptos::*; use std::{rc::Rc, str::FromStr}; use thiserror::Error; @@ -80,7 +81,17 @@ where T: Clone + PartialEq, C: Codec, { - // TODO ssr + cfg_if! { if #[cfg(feature = "ssr")] { + let (data, set_data) = create_signal(None); + let set_value = move |value: Option| { + set_data.set(value); + }; + let value = create_memo(move |_| data.get().unwrap_or_default()); + return (value, set_value); + } else { + // Continue + }} + let UseStorageOptions { codec, on_error, From d56c5cf51430e7d8c7addf77192af204f563f95b Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 16:08:51 +0100 Subject: [PATCH 27/90] Problem: use_storage defaults to StringCodec. Allow turbo fish usage and rely on Default --- src/storage/codec_json.rs | 2 +- src/storage/codec_prost.rs | 2 +- src/storage/mod.rs | 5 +++++ src/storage/use_storage.rs | 43 +++++++++++++++++++++++++++++--------- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs index fc065a7..a8c140f 100644 --- a/src/storage/codec_json.rs +++ b/src/storage/codec_json.rs @@ -1,6 +1,6 @@ use super::{Codec, UseStorageOptions}; -#[derive(Clone, PartialEq)] +#[derive(Clone, Default, PartialEq)] pub struct JsonCodec(); impl Codec for JsonCodec { diff --git a/src/storage/codec_prost.rs b/src/storage/codec_prost.rs index 0b0bfa7..74d1856 100644 --- a/src/storage/codec_prost.rs +++ b/src/storage/codec_prost.rs @@ -2,7 +2,7 @@ use super::{Codec, UseStorageOptions}; use base64::Engine; use thiserror::Error; -#[derive(Clone, PartialEq)] +#[derive(Clone, Default, PartialEq)] pub struct ProstCodec(); #[derive(Error, Debug, PartialEq)] diff --git a/src/storage/mod.rs b/src/storage/mod.rs index eeae93e..e5b09ed 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -4,4 +4,9 @@ mod codec_json; mod codec_prost; mod use_storage; +pub use crate::core::StorageType; +#[cfg(feature = "serde")] +pub use codec_json::*; +#[cfg(feature = "prost")] +pub use codec_prost::*; pub use use_storage::*; diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index d048b9c..27b1452 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -34,17 +34,22 @@ pub enum UseStorageError { } /// Hook for using local storage. Returns a result of a signal and a setter / deleter. -pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> ()) +pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> () + Clone) where - T: Clone + Default + FromStr + PartialEq + ToString, + T: Clone + Default + PartialEq, + C: Codec + Default, { - use_storage_with_options(StorageType::Local, key, UseStorageOptions::string_codec()) + use_storage_with_options( + StorageType::Local, + key, + UseStorageOptions::::default(), + ) } pub fn use_local_storage_with_options( key: impl AsRef, options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> ()) +) -> (Memo, impl Fn(Option) -> () + Clone) where T: Clone + PartialEq, C: Codec, @@ -53,17 +58,24 @@ where } /// Hook for using session storage. Returns a result of a signal and a setter / deleter. -pub fn use_session_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> ()) +pub fn use_session_storage( + key: impl AsRef, +) -> (Memo, impl Fn(Option) -> () + Clone) where - T: Clone + Default + FromStr + PartialEq + ToString, + T: Clone + Default + PartialEq, + C: Codec + Default, { - use_storage_with_options(StorageType::Session, key, UseStorageOptions::string_codec()) + use_storage_with_options( + StorageType::Session, + key, + UseStorageOptions::::default(), + ) } pub fn use_session_storage_with_options( key: impl AsRef, options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> ()) +) -> (Memo, impl Fn(Option) -> () + Clone) where T: Clone + PartialEq, C: Codec, @@ -76,7 +88,7 @@ pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> ()) +) -> (Memo, impl Fn(Option) -> () + Clone) where T: Clone + PartialEq, C: Codec, @@ -206,6 +218,17 @@ fn decode_item>( .unwrap_or_default() } +impl + Default> Default for UseStorageOptions { + fn default() -> Self { + Self { + codec: C::default(), + on_error: Rc::new(|_err| ()), + listen_to_storage_changes: true, + default_value: MaybeSignal::default(), + } + } +} + impl> UseStorageOptions { pub(super) fn new(codec: C) -> Self { Self { @@ -244,7 +267,7 @@ pub trait Codec: Clone + 'static { fn decode(&self, str: String) -> Result; } -#[derive(Clone, PartialEq)] +#[derive(Clone, Default, PartialEq)] pub struct StringCodec(); impl Codec for StringCodec { From 1371b81b6777d40ad4a8fedb63124e6b2f3b1a1b Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 19:51:02 +0100 Subject: [PATCH 28/90] Problem: use_storage value setter differs from existing API too much. Use WriteSignal + separate deleter fn --- src/storage/use_storage.rs | 108 +++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 41 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 27b1452..7a65fee 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -13,7 +13,7 @@ pub struct UseStorageOptions> { codec: C, on_error: Rc)>, listen_to_storage_changes: bool, - default_value: MaybeSignal, + default_value: MaybeRwSignal, } /// Session handling errors returned by [`use_storage`]. @@ -34,7 +34,9 @@ pub enum UseStorageError { } /// Hook for using local storage. Returns a result of a signal and a setter / deleter. -pub fn use_local_storage(key: impl AsRef) -> (Memo, impl Fn(Option) -> () + Clone) +pub fn use_local_storage( + key: impl AsRef, +) -> (Signal, WriteSignal, impl Fn() -> () + Clone) where T: Clone + Default + PartialEq, C: Codec + Default, @@ -49,7 +51,7 @@ where pub fn use_local_storage_with_options( key: impl AsRef, options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() -> () + Clone) where T: Clone + PartialEq, C: Codec, @@ -60,7 +62,7 @@ where /// Hook for using session storage. Returns a result of a signal and a setter / deleter. pub fn use_session_storage( key: impl AsRef, -) -> (Memo, impl Fn(Option) -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() -> () + Clone) where T: Clone + Default + PartialEq, C: Codec + Default, @@ -75,7 +77,7 @@ where pub fn use_session_storage_with_options( key: impl AsRef, options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() -> () + Clone) where T: Clone + PartialEq, C: Codec, @@ -88,7 +90,7 @@ pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, options: UseStorageOptions, -) -> (Memo, impl Fn(Option) -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() -> () + Clone) where T: Clone + PartialEq, C: Codec, @@ -99,7 +101,7 @@ where set_data.set(value); }; let value = create_memo(move |_| data.get().unwrap_or_default()); - return (value, set_value); + return (value, set_value, || ()); } else { // Continue }} @@ -118,7 +120,7 @@ where .and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone)); let storage = handle_error(&on_error, storage); - // Fetch initial value (undecoded) + // Fetch initial value let initial_value = storage .to_owned() // Pull from storage @@ -129,71 +131,95 @@ where handle_error(&on_error, result) }) .unwrap_or_default(); - // Decode initial value let initial_value = decode_item(&codec, initial_value, &on_error); - let (data, set_data) = create_signal(initial_value); + // Data signal: use initial value or falls back to default value. + let (default_value, set_default_value) = default_value.into_signal(); + let (data, set_data) = match initial_value { + Some(initial_value) => { + let (data, set_data) = create_signal(initial_value); + (data.into(), set_data) + } + None => (default_value, set_default_value), + }; - // Update storage value - let set_value = { + // If data is removed from browser storage, revert to default value + let revert_data = move || { + set_data.set(default_value.get_untracked()); + }; + + // Update storage value on change + { let storage = storage.to_owned(); - let key = key.as_ref().to_owned(); let codec = codec.to_owned(); + let key = key.as_ref().to_owned(); let on_error = on_error.to_owned(); - move |value: Option| { - let key = key.as_str(); - // Attempt to update storage - let _ = storage.as_ref().map(|storage| { - let result = match value { - // Update - Some(ref value) => codec + let _ = watch( + move || data.get(), + move |value, _, _| { + let key = key.as_str(); + if let Ok(storage) = &storage { + let result = codec .encode(&value) .map_err(UseStorageError::ItemCodecError) .and_then(|enc_value| { + // Set storage -- this sends an event to other pages storage .set_item(key, &enc_value) .map_err(UseStorageError::SetItemFailed) - }), - // Remove - None => storage - .remove_item(key) - .map_err(UseStorageError::RemoveItemFailed), - }; - handle_error(&on_error, result) - }); - - // Notify signal of change - set_data.set(value); - } + }); + let _ = handle_error(&on_error, result); + } + }, + false, + ); }; // Listen for storage events // Note: we only receive events from other tabs / windows, not from internal updates. if listen_to_storage_changes { let key = key.as_ref().to_owned(); + let on_error = on_error.to_owned(); let _ = use_event_listener_with_options( use_window(), leptos::ev::storage, move |ev| { + let mut deleted = false; // Update storage value if our key matches if let Some(k) = ev.key() { if k == key { - let value = decode_item(&codec, ev.new_value(), &on_error); - set_data.set(value) + match decode_item(&codec, ev.new_value(), &on_error) { + Some(value) => set_data.set(value), + None => deleted = true, + } } } else { // All keys deleted - set_data.set(None) + deleted = true; + } + if deleted { + revert_data(); } }, UseEventListenerOptions::default().passive(true), ); }; - // Apply default value - let value = create_memo(move |_| data.get().unwrap_or_else(|| default_value.get())); + // Remove from storage fn + let remove = { + let key = key.as_ref().to_owned(); + move || { + let _ = storage.as_ref().map(|storage| { + let result = storage + .remove_item(key.as_ref()) + .map_err(UseStorageError::RemoveItemFailed); + let _ = handle_error(&on_error, result); + revert_data(); + }); + } + }; - (value, set_value) + (data, set_data, remove) } /// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. @@ -224,7 +250,7 @@ impl + Default> Default for UseStorageOptions< codec: C::default(), on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, - default_value: MaybeSignal::default(), + default_value: MaybeRwSignal::default(), } } } @@ -235,7 +261,7 @@ impl> UseStorageOptions { codec, on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, - default_value: MaybeSignal::default(), + default_value: MaybeRwSignal::default(), } } @@ -255,7 +281,7 @@ impl> UseStorageOptions { pub fn default_value(self, values: impl Into>) -> Self { Self { - default_value: values.into().into_signal().0.into(), + default_value: values.into(), ..self } } From f23d8ad31cf966f510b92bdb06f50634f241c905 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 27 Oct 2023 20:46:53 +0100 Subject: [PATCH 29/90] Problem: use_color_mode doesn't work with new use_storage API. Use StringCodec --- src/use_color_mode.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index 86efba4..af7e2b8 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -1,6 +1,7 @@ use crate::core::{ElementMaybeSignal, MaybeRwSignal}; use crate::storage::{use_storage_with_options, UseStorageOptions}; use std::fmt::{Display, Formatter}; +use std::str::FromStr; use crate::core::StorageType; use crate::use_preferred_dark; @@ -263,13 +264,14 @@ fn get_store_signal( let (store, set_store) = storage_signal.split(); (store.into(), set_store) } else if storage_enabled { - use_storage_with_options( + let (store, set_store, _) = use_storage_with_options( + storage, storage_key, - initial_value, - UseStorageOptions::default() + UseStorageOptions::string_codec() .listen_to_storage_changes(listen_to_storage_changes) - .storage_type(storage), - ) + .default_value(initial_value), + ); + (store, set_store) } else { initial_value.into_signal() } @@ -306,6 +308,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 From 3ecaade851ae4af5f1a37e5b01c982a66d2f2222 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 28 Oct 2023 12:13:55 +0100 Subject: [PATCH 30/90] Schedule internal use_storage events to notify same page of changes --- src/storage/use_storage.rs | 182 ++++++++++++++++++++++--------------- 1 file changed, 110 insertions(+), 72 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 7a65fee..a225a17 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -1,6 +1,6 @@ use crate::{ core::{MaybeRwSignal, StorageType}, - use_event_listener_with_options, use_window, UseEventListenerOptions, + use_event_listener, use_window, }; use cfg_if::cfg_if; use leptos::*; @@ -8,6 +8,8 @@ use std::{rc::Rc, str::FromStr}; use thiserror::Error; use wasm_bindgen::JsValue; +const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; + #[derive(Clone)] pub struct UseStorageOptions> { codec: C, @@ -29,6 +31,8 @@ pub enum UseStorageError { 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), } @@ -120,88 +124,134 @@ where .and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone)); let storage = handle_error(&on_error, storage); - // Fetch initial value - let initial_value = storage - .to_owned() - // Pull from storage - .and_then(|s| { - let result = s - .get_item(key.as_ref()) - .map_err(UseStorageError::GetItemFailed); - handle_error(&on_error, result) - }) - .unwrap_or_default(); - let initial_value = decode_item(&codec, initial_value, &on_error); - - // Data signal: use initial value or falls back to default value. - let (default_value, set_default_value) = default_value.into_signal(); - let (data, set_data) = match initial_value { - Some(initial_value) => { - let (data, set_data) = create_signal(initial_value); - (data.into(), set_data) + // 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); + }) } - None => (default_value, set_default_value), }; - // If data is removed from browser storage, revert to default value - let revert_data = move || { - set_data.set(default_value.get_untracked()); + // Fires when storage needs to be updated + let notify = create_trigger(); + + // 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(); + prev.map(|prev| prev + 1).unwrap_or_default() + }); + + // Fetch from storage and falls back to the default (possibly a signal) if deleted + let fetcher = { + 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 (default, _) = default_value.into_signal(); + create_memo(move |_| { + notify.track(); + 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(()) + // Fallback to default + .unwrap_or_else(move || default.get()) + }) }; - // Update storage value on change + // Create mutable data signal from our fetcher + let (data, set_data) = MaybeRwSignal::::from(fetcher).into_signal(); + let data = create_memo(move |_| data.get()); + + // Set storage value on data change { 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( - move || data.get(), - move |value, _, _| { - let key = key.as_str(); + 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) + .encode(value) .map_err(UseStorageError::ItemCodecError) .and_then(|enc_value| { - // Set storage -- this sends an event to other pages + // Set storage -- sends a global event storage - .set_item(key, &enc_value) + .set_item(&key, &enc_value) .map_err(UseStorageError::SetItemFailed) }); - let _ = handle_error(&on_error, result); + let result = handle_error(&on_error, result); + // Send internal storage event + if result.is_ok() { + dispatch_storage_event(); + } } }, false, ); }; - // Listen for storage events - // Note: we only receive events from other tabs / windows, not from internal updates. if listen_to_storage_changes { - let key = key.as_ref().to_owned(); - let on_error = on_error.to_owned(); - let _ = use_event_listener_with_options( + 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(), - leptos::ev::storage, - move |ev| { - let mut deleted = false; - // Update storage value if our key matches - if let Some(k) = ev.key() { - if k == key { - match decode_item(&codec, ev.new_value(), &on_error) { - Some(value) => set_data.set(value), - None => deleted = true, - } - } - } else { - // All keys deleted - deleted = true; - } - if deleted { - revert_data(); + ev::Custom::new(INTERNAL_STORAGE_EVENT), + move |ev: web_sys::CustomEvent| { + if Some(check_key.clone()) == ev.detail().as_string() { + notify.notify() } }, - UseEventListenerOptions::default().passive(true), ); }; @@ -210,16 +260,18 @@ where let key = key.as_ref().to_owned(); move || { let _ = storage.as_ref().map(|storage| { + // Delete directly from storage let result = storage - .remove_item(key.as_ref()) + .remove_item(&key) .map_err(UseStorageError::RemoveItemFailed); let _ = handle_error(&on_error, result); - revert_data(); + notify.notify(); + dispatch_storage_event(); }); } }; - (data, set_data, remove) + (data.into(), set_data, remove) } /// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. @@ -230,20 +282,6 @@ fn handle_error( result.or_else(|err| Err((on_error)(err))) } -fn decode_item>( - codec: &C, - str: Option, - on_error: &Rc)>, -) -> Option { - str.map(|str| { - let result = codec.decode(str).map_err(UseStorageError::ItemCodecError); - handle_error(&on_error, result) - }) - .transpose() - // We've sent our error so unwrap to drop () error - .unwrap_or_default() -} - impl + Default> Default for UseStorageOptions { fn default() -> Self { Self { From c62adedcf2d9868edb31f92138057a68a9e07fed Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 28 Oct 2023 12:16:05 +0100 Subject: [PATCH 31/90] Update use_storage example for new API --- examples/use_storage/src/main.rs | 40 ++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/examples/use_storage/src/main.rs b/examples/use_storage/src/main.rs index 7f5442f..849f8fa 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" /> +

"Second " From c24f0a5d45247e4c2ce6b091835a60ae77aff91a Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 28 Oct 2023 12:19:33 +0100 Subject: [PATCH 32/90] Separate StringCodec to separate file --- src/storage/codec_string.rs | 36 ++++++++++++++++++++++++++++ src/storage/mod.rs | 2 ++ src/storage/use_storage.rs | 48 ++++++------------------------------- 3 files changed, 45 insertions(+), 41 deletions(-) create mode 100644 src/storage/codec_string.rs diff --git a/src/storage/codec_string.rs b/src/storage/codec_string.rs new file mode 100644 index 0000000..2d855df --- /dev/null +++ b/src/storage/codec_string.rs @@ -0,0 +1,36 @@ +use super::{Codec, UseStorageOptions}; +use std::str::FromStr; + +#[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) + } +} + +impl UseStorageOptions { + pub fn string_codec() -> Self { + Self::new(StringCodec()) + } +} + +#[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 e5b09ed..7183c63 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -2,6 +2,7 @@ mod codec_json; #[cfg(feature = "prost")] mod codec_prost; +mod codec_string; mod use_storage; pub use crate::core::StorageType; @@ -9,4 +10,5 @@ pub use crate::core::StorageType; pub use codec_json::*; #[cfg(feature = "prost")] pub use codec_prost::*; +pub use codec_string::*; pub use use_storage::*; diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index a225a17..cd2a26b 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -4,12 +4,18 @@ use crate::{ }; use cfg_if::cfg_if; use leptos::*; -use std::{rc::Rc, str::FromStr}; +use std::rc::Rc; use thiserror::Error; use wasm_bindgen::JsValue; const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; +pub trait Codec: Clone + 'static { + type Error; + fn encode(&self, val: &T) -> Result; + fn decode(&self, str: String) -> Result; +} + #[derive(Clone)] pub struct UseStorageOptions> { codec: C, @@ -324,43 +330,3 @@ impl> UseStorageOptions { } } } - -pub trait Codec: Clone + 'static { - type Error; - fn encode(&self, val: &T) -> Result; - fn decode(&self, str: String) -> Result; -} - -#[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) - } -} - -impl UseStorageOptions { - pub fn string_codec() -> Self { - Self::new(StringCodec()) - } -} - -#[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)); - } -} From 50952581de4c8cc77d27397ed233418deffc0b31 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 28 Oct 2023 12:28:26 +0100 Subject: [PATCH 33/90] Clean up of UseStorageOptions default --- src/storage/use_storage.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index cd2a26b..cd08b48 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -288,18 +288,13 @@ fn handle_error( result.or_else(|err| Err((on_error)(err))) } -impl + Default> Default for UseStorageOptions { +impl + Default> Default for UseStorageOptions { fn default() -> Self { - Self { - codec: C::default(), - on_error: Rc::new(|_err| ()), - listen_to_storage_changes: true, - default_value: MaybeRwSignal::default(), - } + Self::new(C::default()) } } -impl> UseStorageOptions { +impl> UseStorageOptions { pub(super) fn new(codec: C) -> Self { Self { codec, From 7084b91c0abce11b3c0595a3a5272f2763e742b4 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Sat, 28 Oct 2023 16:18:29 -0500 Subject: [PATCH 34/90] made ElementMaybeSignal's from string conversion SSR safe. Fixes #41 --- CHANGELOG.md | 7 ++++ Cargo.toml | 2 +- examples/ssr/src/app.rs | 10 ++++- examples/ssr/style/main.scss | 9 ++++- src/core/element_maybe_signal.rs | 21 +++++++--- src/core/elements_maybe_signal.rs | 64 ++++++++++++++++++++----------- src/on_click_outside.rs | 2 +- src/use_color_mode.rs | 2 +- src/use_css_var.rs | 2 +- src/use_draggable.rs | 2 +- src/use_event_listener.rs | 2 +- src/use_intersection_observer.rs | 2 +- src/use_mutation_observer.rs | 2 +- src/use_resize_observer.rs | 2 +- 14 files changed, 88 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be42cc6..82633c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.1] - 2023-10-28 + +### Fixes 🍕 + +- Using strings for `ElementMaybeSignal` and `ElementsMaybeSignal` is now SSR safe. + - This fixes specifically `use_color_mode` to work on the server. + ## [0.8.0] - 2023-10-24 ### New Functions 🚀 diff --git a/Cargo.toml b/Cargo.toml index 9b19c02..5c557e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos-use" -version = "0.8.0" +version = "0.8.1" edition = "2021" authors = ["Marc-Stefan Cassola"] categories = ["gui", "web-programming"] diff --git a/examples/ssr/src/app.rs b/examples/ssr/src/app.rs index 29e62fc..20f3b91 100644 --- a/examples/ssr/src/app.rs +++ b/examples/ssr/src/app.rs @@ -5,8 +5,8 @@ use leptos_meta::*; use leptos_router::*; use leptos_use::storage::use_local_storage; use leptos_use::{ - use_debounce_fn, use_event_listener, use_intl_number_format, use_window, - UseIntlNumberFormatOptions, + use_color_mode, use_debounce_fn, use_event_listener, use_intl_number_format, use_window, + ColorMode, UseColorModeReturn, UseIntlNumberFormatOptions, }; #[component] @@ -63,11 +63,17 @@ fn HomePage() -> impl IntoView { ); debounced_fn(); + let UseColorModeReturn { mode, set_mode, .. } = use_color_mode(); + view! {

Leptos-Use SSR Example

Locale zh-Hans-CN-u-nu-hanidec: {zh_count}

Press any key: {key}

Debounced called: {debounce_value}

+

Color mode: {move || format!("{:?}", mode.get())}

+ + + } } diff --git a/examples/ssr/style/main.scss b/examples/ssr/style/main.scss index e4538e1..24ed267 100644 --- a/examples/ssr/style/main.scss +++ b/examples/ssr/style/main.scss @@ -1,4 +1,9 @@ body { - font-family: sans-serif; - text-align: center; + font-family: sans-serif; + text-align: center; +} + +.dark { + background-color: black; + color: white; } \ No newline at end of file diff --git a/src/core/element_maybe_signal.rs b/src/core/element_maybe_signal.rs index 9d972a7..20dd423 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,11 @@ 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")] { + Self::Static(None) + } else { + Self::Static(document().query_selector(target).unwrap_or_default()) + }} } } @@ -186,7 +191,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 +200,14 @@ 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")] { + 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/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/use_color_mode.rs b/src/use_color_mode.rs index 3bffcb5..1c65314 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -160,7 +160,7 @@ where } }); - let target = (target).into(); + let target = target.into(); let update_html_attrs = { move |target: ElementMaybeSignal, 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_draggable.rs b/src/use_draggable.rs index 5954243..c44aef8 100644 --- a/src/use_draggable.rs +++ b/src/use_draggable.rs @@ -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(); 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_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_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_resize_observer.rs b/src/use_resize_observer.rs index 1c61081..2e35e85 100644 --- a/src/use_resize_observer.rs +++ b/src/use_resize_observer.rs @@ -114,7 +114,7 @@ where } }; - let targets = (target).into(); + let targets = target.into(); let stop_watch = { let cleanup = cleanup.clone(); From 40fdb5f6b55f2fe029453b13b49d84183041aad2 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 09:45:43 +0000 Subject: [PATCH 35/90] Add use_storage debounce / throttle on write --- src/storage/use_storage.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index cd08b48..498841f 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -1,6 +1,8 @@ use crate::{ core::{MaybeRwSignal, StorageType}, use_event_listener, use_window, + utils::FilterOptions, + watch_with_options, WatchOptions, }; use cfg_if::cfg_if; use leptos::*; @@ -16,12 +18,12 @@ pub trait Codec: Clone + 'static { fn decode(&self, str: String) -> Result; } -#[derive(Clone)] pub struct UseStorageOptions> { codec: C, on_error: Rc)>, listen_to_storage_changes: bool, default_value: MaybeRwSignal, + filter: FilterOptions, } /// Session handling errors returned by [`use_storage`]. @@ -121,6 +123,7 @@ where on_error, listen_to_storage_changes, default_value, + filter, } = options; // Get storage API @@ -208,7 +211,7 @@ where 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( + 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. @@ -234,7 +237,7 @@ where } } }, - false, + WatchOptions::default().filter(filter), ); }; @@ -301,6 +304,7 @@ impl> UseStorageOptions { on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, default_value: MaybeRwSignal::default(), + filter: FilterOptions::default(), } } @@ -324,4 +328,11 @@ impl> UseStorageOptions { ..self } } + + pub fn filter(self, filter: impl Into) -> Self { + Self { + filter: filter.into(), + ..self + } + } } From 4110a763c970b602000fd78e02271f5d35953d67 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 11:41:59 +0000 Subject: [PATCH 36/90] Document use_storage with examples --- src/storage/codec_json.rs | 22 +++++++++ src/storage/codec_prost.rs | 29 ++++++++++++ src/storage/codec_string.rs | 18 +++++++- src/storage/use_storage.rs | 90 +++++++++++++++++++++++++++++++++++-- 4 files changed, 154 insertions(+), 5 deletions(-) diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs index a8c140f..d3b77b8 100644 --- a/src/storage/codec_json.rs +++ b/src/storage/codec_json.rs @@ -1,5 +1,26 @@ use super::{Codec, UseStorageOptions}; +/// 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_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; +/// # 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! { } +/// # } +/// ``` #[derive(Clone, Default, PartialEq)] pub struct JsonCodec(); @@ -18,6 +39,7 @@ impl Codec for JsonCodec { impl UseStorageOptions { + /// Constructs a new `UseStorageOptions` with a [`JsonCodec`] for JSON messages. pub fn json_codec() -> Self { Self::new(JsonCodec()) } diff --git a/src/storage/codec_prost.rs b/src/storage/codec_prost.rs index 74d1856..ced8388 100644 --- a/src/storage/codec_prost.rs +++ b/src/storage/codec_prost.rs @@ -2,6 +2,34 @@ use super::{Codec, UseStorageOptions}; 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_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; +/// # use serde::{Deserialize, Serialize}; +/// # +/// # 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(); @@ -30,6 +58,7 @@ impl Codec for ProstCodec { } impl UseStorageOptions { + /// Constructs a new `UseStorageOptions` with a [`ProstCodec`] for ProtoBuf messages. pub fn prost_codec() -> Self { Self::new(ProstCodec()) } diff --git a/src/storage/codec_string.rs b/src/storage/codec_string.rs index 2d855df..a2dd64b 100644 --- a/src/storage/codec_string.rs +++ b/src/storage/codec_string.rs @@ -1,6 +1,21 @@ use super::{Codec, UseStorageOptions}; 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_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; +/// # use serde::{Deserialize, Serialize}; +/// # +/// # pub fn Demo() -> impl IntoView { +/// let (get, set, remove) = use_local_storage::("my-key"); +/// # view! { } +/// # } +/// ``` #[derive(Clone, Default, PartialEq)] pub struct StringCodec(); @@ -16,7 +31,8 @@ impl Codec for StringCodec { } } -impl UseStorageOptions { +impl UseStorageOptions { + /// Constructs a new `UseStorageOptions` with a [`StringCodec`] relying on [`FromStr`] to parse. pub fn string_codec() -> Self { Self::new(StringCodec()) } diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 498841f..108f911 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -12,12 +12,17 @@ use wasm_bindgen::JsValue; const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; +/// A codec for encoding and decoding values to and from UTF-16 strings. These strings are then stored in browser storage. 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; } +/// Options for use with [`use_local_storage_with_options`], [`use_session_storage_with_options`] and [`use_storage_with_options`]. pub struct UseStorageOptions> { codec: C, on_error: Rc)>, @@ -45,7 +50,13 @@ pub enum UseStorageError { ItemCodecError(Err), } -/// Hook for using local storage. Returns a result of a signal and a setter / deleter. +/// 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. +/// +/// See [`use_storage_with_options`] for more details on how to use. pub fn use_local_storage( key: impl AsRef, ) -> (Signal, WriteSignal, impl Fn() -> () + Clone) @@ -60,6 +71,7 @@ where ) } +/// Accepts [`UseStorageOptions`]. See [`use_local_storage`] for details. pub fn use_local_storage_with_options( key: impl AsRef, options: UseStorageOptions, @@ -71,7 +83,13 @@ where use_storage_with_options(StorageType::Local, key, options) } -/// Hook for using session storage. Returns a result of a signal and a setter / deleter. +/// 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. +/// +/// See [`use_storage_with_options`] for more details on how to use. pub fn use_session_storage( key: impl AsRef, ) -> (Signal, WriteSignal, impl Fn() -> () + Clone) @@ -86,6 +104,7 @@ where ) } +/// Accepts [`UseStorageOptions`]. See [`use_session_storage`] for details. pub fn use_session_storage_with_options( key: impl AsRef, options: UseStorageOptions, @@ -97,7 +116,65 @@ where use_storage_with_options(StorageType::Session, key, options) } -/// Hook for using any kind of storage. Returns a result of a signal and a setter / deleter. +/// Reactive [Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). +/// +/// * [See a demo](https://leptos-use.rs/storage/use_storage.html) +/// * [See a full example](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_storage) +/// +/// ## Usage +/// +/// 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`].Finally, see [`UseStorageOptions`] to see how behaviour can be further customised. +/// +/// Returns a triplet `(read_signal, write_signal, delete_from_storage_fn)`. +/// +/// Signals work as expected and can be used to read and write to storage. The `delete_from_storage_fn` can be called to delete the item from storage. Once deleted the signals will revert back to the default value. +/// +/// ## Example +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; +/// # use serde::{Deserialize, Serialize}; +/// # +/// # pub fn Demo() -> impl IntoView { +/// // Binds a struct: +/// let (state, set_state, _) = use_local_storage::("my-state"); +/// +/// // Binds a bool, stored as a string: +/// let (flag, set_flag, remove_flag) = use_session_storage::("my-flag"); +/// +/// // 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 stored in ProtoBuf format: +/// let (id, set_id, _) = use_storage_with_options::( +/// StorageType::Session, +/// "my-id", +/// UseStorageOptions::prost_codec(), +/// ); +/// # view! { } +/// # } +/// +/// // Data stored in JSON must implement Serialize, Deserialize: +/// #[derive(Serialize, Deserialize, Clone, PartialEq)] +/// pub struct MyState { +/// pub hello: String, +/// pub greeting: String, +/// } +/// +/// // 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() +/// } +/// } +/// } +/// ``` pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, @@ -107,6 +184,7 @@ where T: Clone + PartialEq, C: Codec, { + /* cfg_if! { if #[cfg(feature = "ssr")] { let (data, set_data) = create_signal(None); let set_value = move |value: Option| { @@ -116,7 +194,7 @@ where return (value, set_value, || ()); } else { // Continue - }} + }}*/ let UseStorageOptions { codec, @@ -308,6 +386,7 @@ impl> UseStorageOptions { } } + /// 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), @@ -315,6 +394,7 @@ impl> UseStorageOptions { } } + /// 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, @@ -322,6 +402,7 @@ impl> UseStorageOptions { } } + /// Default value to use when the storage key is not set. Accepts a signal. pub fn default_value(self, values: impl Into>) -> Self { Self { default_value: values.into(), @@ -329,6 +410,7 @@ impl> UseStorageOptions { } } + /// Debounce or throttle the writing to storage whenever the value changes. pub fn filter(self, filter: impl Into) -> Self { Self { filter: filter.into(), From 6af4cc0693f709995c3453737715d34bf46d4f1b Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 11:49:03 +0000 Subject: [PATCH 37/90] Switch to relying on a codec's default instead of constructors on UseStorageOptions --- src/storage/codec_json.rs | 11 +---------- src/storage/codec_prost.rs | 9 +-------- src/storage/codec_string.rs | 9 +-------- src/storage/use_storage.rs | 10 +++------- src/use_color_mode.rs | 6 +++--- 5 files changed, 9 insertions(+), 36 deletions(-) diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs index d3b77b8..30cd03a 100644 --- a/src/storage/codec_json.rs +++ b/src/storage/codec_json.rs @@ -1,4 +1,4 @@ -use super::{Codec, UseStorageOptions}; +use super::Codec; /// A codec for storing JSON messages that relies on [`serde_json`] to parse. /// @@ -36,15 +36,6 @@ impl Codec for JsonCodec { } } -impl - UseStorageOptions -{ - /// Constructs a new `UseStorageOptions` with a [`JsonCodec`] for JSON messages. - pub fn json_codec() -> Self { - Self::new(JsonCodec()) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/storage/codec_prost.rs b/src/storage/codec_prost.rs index ced8388..41a55d9 100644 --- a/src/storage/codec_prost.rs +++ b/src/storage/codec_prost.rs @@ -1,4 +1,4 @@ -use super::{Codec, UseStorageOptions}; +use super::Codec; use base64::Engine; use thiserror::Error; @@ -57,13 +57,6 @@ impl Codec for ProstCodec { } } -impl UseStorageOptions { - /// Constructs a new `UseStorageOptions` with a [`ProstCodec`] for ProtoBuf messages. - pub fn prost_codec() -> Self { - Self::new(ProstCodec()) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/storage/codec_string.rs b/src/storage/codec_string.rs index a2dd64b..91ac731 100644 --- a/src/storage/codec_string.rs +++ b/src/storage/codec_string.rs @@ -1,4 +1,4 @@ -use super::{Codec, UseStorageOptions}; +use super::Codec; use std::str::FromStr; /// A codec for strings that relies on [`FromStr`] and [`ToString`] to parse. @@ -31,13 +31,6 @@ impl Codec for StringCodec { } } -impl UseStorageOptions { - /// Constructs a new `UseStorageOptions` with a [`StringCodec`] relying on [`FromStr`] to parse. - pub fn string_codec() -> Self { - Self::new(StringCodec()) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 108f911..14936f8 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -371,21 +371,17 @@ fn handle_error( impl + Default> Default for UseStorageOptions { fn default() -> Self { - Self::new(C::default()) - } -} - -impl> UseStorageOptions { - pub(super) fn new(codec: C) -> Self { Self { - codec, + codec: C::default(), on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, default_value: MaybeRwSignal::default(), filter: FilterOptions::default(), } } +} +impl> UseStorageOptions { /// Optional callback whenever an error occurs. pub fn on_error(self, on_error: impl Fn(UseStorageError) + 'static) -> Self { Self { diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index af7e2b8..9c9d567 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -1,5 +1,5 @@ use crate::core::{ElementMaybeSignal, MaybeRwSignal}; -use crate::storage::{use_storage_with_options, UseStorageOptions}; +use crate::storage::{use_storage_with_options, StringCodec, UseStorageOptions}; use std::fmt::{Display, Formatter}; use std::str::FromStr; @@ -264,10 +264,10 @@ fn get_store_signal( let (store, set_store) = storage_signal.split(); (store.into(), set_store) } else if storage_enabled { - let (store, set_store, _) = use_storage_with_options( + let (store, set_store, _) = use_storage_with_options::( storage, storage_key, - UseStorageOptions::string_codec() + UseStorageOptions::default() .listen_to_storage_changes(listen_to_storage_changes) .default_value(initial_value), ); From 64c6f63d681599c26f7e3c233883a9d7ce8e1951 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 11:57:02 +0000 Subject: [PATCH 38/90] Fix dead doc ref --- src/storage/use_storage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 14936f8..5ee9ab0 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -31,7 +31,7 @@ pub struct UseStorageOptions> { filter: FilterOptions, } -/// Session handling errors returned by [`use_storage`]. +/// Session handling errors returned by [`use_storage_with_options`]. #[derive(Error, Debug)] pub enum UseStorageError { #[error("storage not available")] From d267408116d2bee4f9df88f91dbf29bf92d625af Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 11:58:14 +0000 Subject: [PATCH 39/90] Add codec setter to UseStorageOptions --- src/storage/use_storage.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 5ee9ab0..7ae49e9 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -382,6 +382,14 @@ impl + Default> Default for UseStorageOptions { } 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 { From f3ea5dcd74cddd37d49e3615bcfb9e0f3230890c Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 12:18:45 +0000 Subject: [PATCH 40/90] Fix use_storage for SSR --- src/storage/use_storage.rs | 310 ++++++++++++++++++------------------- 1 file changed, 153 insertions(+), 157 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 7ae49e9..6ac5875 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -184,18 +184,6 @@ where T: Clone + PartialEq, C: Codec, { - /* - cfg_if! { if #[cfg(feature = "ssr")] { - let (data, set_data) = create_signal(None); - let set_value = move |value: Option| { - set_data.set(value); - }; - let value = create_memo(move |_| data.get().unwrap_or_default()); - return (value, set_value, || ()); - } else { - // Continue - }}*/ - let UseStorageOptions { codec, on_error, @@ -203,162 +191,170 @@ where default_value, filter, } = options; + let (default, _) = default_value.into_signal(); - // 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); + cfg_if! { if #[cfg(feature = "ssr")] { + let (data, set_data) = create_signal(default.get_untracked()); + let remove = move || { + set_data.set(default.get_untracked()); + }; + (data.into(), set_data, remove) + } else { + // 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); - // 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(); + // 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(); - 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, + 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"), ) - .expect("failed to create custom storage event"), - ) - .map_err(UseStorageError::NotifyItemChangedFailed); - let _ = handle_error(&on_error, result); - }) - } - }; - - // Fires when storage needs to be updated - let notify = create_trigger(); - - // 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(); - prev.map(|prev| prev + 1).unwrap_or_default() - }); - - // Fetch from storage and falls back to the default (possibly a signal) if deleted - let fetcher = { - 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 (default, _) = default_value.into_signal(); - create_memo(move |_| { - notify.track(); - 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) + .map_err(UseStorageError::NotifyItemChangedFailed); + let _ = 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(()) - // Fallback to default - .unwrap_or_else(move || default.get()) - }) - }; - - // Create mutable data signal from our fetcher - let (data, set_data) = MaybeRwSignal::::from(fetcher).into_signal(); - let data = create_memo(move |_| data.get()); - - // Set storage value on data change - { - 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() } + }; + + // Fires when storage needs to be updated + let notify = create_trigger(); + + // 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(); + prev.map(|prev| prev + 1).unwrap_or_default() }); - // 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() { + + // Fetch from storage and falls back to the default (possibly a signal) if deleted + let fetcher = { + let storage = storage.to_owned(); + let codec = codec.to_owned(); + let key = key.as_ref().to_owned(); + let on_error = on_error.to_owned(); + create_memo(move |_| { + notify.track(); + 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(()) + // Fallback to default + .unwrap_or_else(move || default.get()) + }) + }; + + // Create mutable data signal from our fetcher + let (data, set_data) = MaybeRwSignal::::from(fetcher).into_signal(); + let data = create_memo(move |_| data.get()); + + // Set storage value on data change + { + 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() } - }, - ); - }; - - // 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(); }); - } - }; + // 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() + } + }, + ); + }; - (data.into(), set_data, remove) + // 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.into(), set_data, remove) + }} } /// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. From 1a27eb0034dc242d147b60bb6b6c0f7a6f58f778 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 12:20:23 +0000 Subject: [PATCH 41/90] Problem: docs test broken with missing codec constructors --- src/storage/use_storage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 6ac5875..546e2c0 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -152,7 +152,7 @@ where /// let (id, set_id, _) = use_storage_with_options::( /// StorageType::Session, /// "my-id", -/// UseStorageOptions::prost_codec(), +/// UseStorageOptions::default(), /// ); /// # view! { } /// # } From 8168d301e012c67addb3221cdde40fe1252d54f8 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 12:23:33 +0000 Subject: [PATCH 42/90] Update prost --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4de6bbf..9851503 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ lazy_static = "1" leptos = "0.5" num = { version = "0.4", optional = true } paste = "1" -prost = { version = "0.11", optional = true } +prost = { version = "0.12", optional = true } serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } thiserror = "1.0" From 208451f0c2366c07eac542c69aefdf0c6af9f702 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 15:44:20 +0000 Subject: [PATCH 43/90] Update docs to specify that values are synced across multiple use_storage calls on the same page and other tabs / windows --- examples/use_storage/src/main.rs | 2 +- src/storage/use_storage.rs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/use_storage/src/main.rs b/examples/use_storage/src/main.rs index 849f8fa..36abca4 100644 --- a/examples/use_storage/src/main.rs +++ b/examples/use_storage/src/main.rs @@ -71,7 +71,7 @@ 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/src/storage/use_storage.rs b/src/storage/use_storage.rs index 546e2c0..dd74213 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -123,12 +123,10 @@ where /// /// ## Usage /// -/// 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`].Finally, see [`UseStorageOptions`] to see how behaviour can be further customised. +/// 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. /// /// Returns a triplet `(read_signal, write_signal, delete_from_storage_fn)`. /// -/// Signals work as expected and can be used to read and write to storage. The `delete_from_storage_fn` can be called to delete the item from storage. Once deleted the signals will revert back to the default value. -/// /// ## Example /// /// ``` From daab6f944cc0daf41ddf7661688d61bf141ce5ab Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 15:47:52 +0000 Subject: [PATCH 44/90] Remove notice to enable feature storage on use_color_mode --- src/use_color_mode.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index 23d407f..5867bf4 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -13,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) @@ -55,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 /// # From 6e423753b863d2b26f58c397c0aa2b503d18345c Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 15:58:41 +0000 Subject: [PATCH 45/90] Problem: UseColorModeOptions::storage_enabled references storage flag / feature which no longer exists --- src/use_color_mode.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index 5867bf4..ff7031f 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -361,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 From a28bbb33e4c69b18fe8fcf9267ab0beca6ece3d1 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sun, 29 Oct 2023 12:34:11 +0000 Subject: [PATCH 46/90] Update docs/book for use_storage --- .../storage/{use_storage.md => use_storage_with_options.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename docs/book/src/storage/{use_storage.md => use_storage_with_options.md} (54%) diff --git a/docs/book/src/storage/use_storage.md b/docs/book/src/storage/use_storage_with_options.md similarity index 54% rename from docs/book/src/storage/use_storage.md rename to docs/book/src/storage/use_storage_with_options.md index f937ee4..26c94e7 100644 --- a/docs/book/src/storage/use_storage.md +++ b/docs/book/src/storage/use_storage_with_options.md @@ -1,3 +1,3 @@ -# use_storage +# use_storage_with_options - + From 389b6e681140df6377b9f37c56c6756fecbf70f3 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 13:30:36 +0000 Subject: [PATCH 47/90] Directly use and return read / write signal given by default_value --- src/storage/use_storage.rs | 67 +++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index dd74213..31fa82d 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -189,12 +189,13 @@ where default_value, filter, } = options; - let (default, _) = default_value.into_signal(); + + let (data, set_data) = default_value.into_signal(); + let default = data.get_untracked(); cfg_if! { if #[cfg(feature = "ssr")] { - let (data, set_data) = create_signal(default.get_untracked()); let remove = move || { - set_data.set(default.get_untracked()); + set_data.set(default.clone()); }; (data.into(), set_data, remove) } else { @@ -230,24 +231,14 @@ where } }; - // Fires when storage needs to be updated - let notify = create_trigger(); - - // 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(); - prev.map(|prev| prev + 1).unwrap_or_default() - }); - - // Fetch from storage and falls back to the default (possibly a signal) if deleted - let fetcher = { + // 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(); - create_memo(move |_| { - notify.track(); - storage + move || { + let fetched = storage .to_owned() .and_then(|storage| { // Get directly from storage @@ -265,17 +256,41 @@ where handle_error(&on_error, result) }) .transpose() - .unwrap_or_default() // Drop handled Err(()) - // Fallback to default - .unwrap_or_else(move || default.get()) - }) + .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()), + }; + } }; - // Create mutable data signal from our fetcher - let (data, set_data) = MaybeRwSignal::::from(fetcher).into_signal(); - let data = create_memo(move |_| data.get()); + // Fetch initial value + fetch_from_storage(); - // Set storage value on data change + // 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(); @@ -310,7 +325,7 @@ where }, WatchOptions::default().filter(filter), ); - }; + } if listen_to_storage_changes { let check_key = key.as_ref().to_owned(); From 16ad1417cfc92ff5656067274b8cb555dad61d98 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 13:33:02 +0000 Subject: [PATCH 48/90] Add link to open new window for use_storage example --- examples/use_storage/src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/use_storage/src/main.rs b/examples/use_storage/src/main.rs index 36abca4..c1a98fa 100644 --- a/examples/use_storage/src/main.rs +++ b/examples/use_storage/src/main.rs @@ -71,7 +71,9 @@ fn Demo() -> impl IntoView {
{move || format!("{:#?}", state2.get())}
- "The values are persistent. When you reload the page or open a second window, 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." } } From af653595a5224d1b4ea7db10ee9af70d5c3a194e Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 13:36:01 +0000 Subject: [PATCH 49/90] Problem: use_storage examples have unused imports --- src/storage/codec_json.rs | 2 +- src/storage/codec_prost.rs | 3 +-- src/storage/codec_string.rs | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs index 30cd03a..7e70e02 100644 --- a/src/storage/codec_json.rs +++ b/src/storage/codec_json.rs @@ -5,7 +5,7 @@ use super::Codec; /// ## Example /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, JsonCodec}; /// # use serde::{Deserialize, Serialize}; /// # /// # pub fn Demo() -> impl IntoView { diff --git a/src/storage/codec_prost.rs b/src/storage/codec_prost.rs index 41a55d9..28986b2 100644 --- a/src/storage/codec_prost.rs +++ b/src/storage/codec_prost.rs @@ -11,8 +11,7 @@ use thiserror::Error; /// ## Example /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; -/// # use serde::{Deserialize, Serialize}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, ProstCodec}; /// # /// # pub fn Demo() -> impl IntoView { /// // Primitive types: diff --git a/src/storage/codec_string.rs b/src/storage/codec_string.rs index 91ac731..2c16f34 100644 --- a/src/storage/codec_string.rs +++ b/src/storage/codec_string.rs @@ -8,8 +8,7 @@ use std::str::FromStr; /// ## Example /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; -/// # use serde::{Deserialize, Serialize}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec}; /// # /// # pub fn Demo() -> impl IntoView { /// let (get, set, remove) = use_local_storage::("my-key"); From 9a9f1ba7d92d973481d459fbde26380e67106c0e Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 13:50:41 +0000 Subject: [PATCH 50/90] Problem: use_storage's default_value with a signal might suggest changing the signal also changes the value used on an unset storage value. Rename to initial value and document this behaviour --- src/storage/use_storage.rs | 14 +++++++------- src/use_color_mode.rs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 31fa82d..b1dbd24 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -27,7 +27,7 @@ pub struct UseStorageOptions> { codec: C, on_error: Rc)>, listen_to_storage_changes: bool, - default_value: MaybeRwSignal, + initial_value: MaybeRwSignal, filter: FilterOptions, } @@ -186,11 +186,11 @@ where codec, on_error, listen_to_storage_changes, - default_value, + initial_value, filter, } = options; - let (data, set_data) = default_value.into_signal(); + let (data, set_data) = initial_value.into_signal(); let default = data.get_untracked(); cfg_if! { if #[cfg(feature = "ssr")] { @@ -384,7 +384,7 @@ impl + Default> Default for UseStorageOptions { codec: C::default(), on_error: Rc::new(|_err| ()), listen_to_storage_changes: true, - default_value: MaybeRwSignal::default(), + initial_value: MaybeRwSignal::default(), filter: FilterOptions::default(), } } @@ -415,10 +415,10 @@ impl> UseStorageOptions { } } - /// Default value to use when the storage key is not set. Accepts a signal. - pub fn default_value(self, values: impl Into>) -> 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 { - default_value: values.into(), + initial_value: initial.into(), ..self } } diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index ff7031f..5f2b781 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -266,7 +266,7 @@ fn get_store_signal( storage_key, UseStorageOptions::default() .listen_to_storage_changes(listen_to_storage_changes) - .default_value(initial_value), + .initial_value(initial_value), ); (store, set_store) } else { From 7c3ebf09f4e5f65c1e22638736d1eae3120c0f90 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 15:43:36 +0000 Subject: [PATCH 51/90] Add overview of codec versioning --- src/storage/use_storage.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index b1dbd24..f640d25 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -12,7 +12,21 @@ use wasm_bindgen::JsValue; const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; -/// A codec for encoding and decoding values to and from UTF-16 strings. These strings are then stored in browser storage. +/// 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; @@ -125,6 +139,8 @@ where /// /// 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. /// +/// 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 From 808e97c072c990570245f9f243ce0f2993702b4a Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 15:44:09 +0000 Subject: [PATCH 52/90] Add JSON versioning docs + examples --- src/storage/codec_json.rs | 92 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs index 7e70e02..c12d3a7 100644 --- a/src/storage/codec_json.rs +++ b/src/storage/codec_json.rs @@ -21,6 +21,98 @@ use super::Codec; /// # 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_with_options, 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_with_options, 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(); From a65e4ddaa1b80be3d06176dd24bb2bb242230337 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 4 Nov 2023 16:33:43 +0000 Subject: [PATCH 53/90] Problem: mdbook refers to use_storage from older API --- docs/book/src/SUMMARY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 4a9c0c5..8a56599 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -10,7 +10,7 @@ - [use_local_storage](storage/use_local_storage.md) - [use_session_storage](storage/use_session_storage.md) -- [use_storage](storage/use_storage.md) +- [use_storage_with_options](storage/use_storage_with_options.md) # Elements From 40b369d36a5061f79a790cee7d30f7e5d942b1d0 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Tue, 7 Nov 2023 10:13:55 +0000 Subject: [PATCH 54/90] Remove use of --cfg=web_sys_unstable_apis --- .cargo/config.toml | 3 --- Cargo.toml | 3 --- examples/.cargo/config.toml | 2 -- examples/use_element_size/.cargo/config.toml | 2 -- src/lib.rs | 14 ++++---------- src/use_element_size.rs | 7 ++----- src/use_resize_observer.rs | 7 ++----- template/.ffizer.yaml | 4 ---- .../{{ module }}/{{ function_name }}.ffizer.hbs.rs | 3 --- 9 files changed, 8 insertions(+), 37 deletions(-) delete mode 100644 .cargo/config.toml delete mode 100644 examples/.cargo/config.toml delete mode 100644 examples/use_element_size/.cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index b891103..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,3 +0,0 @@ -[unstable] -rustflags = ["--cfg=web_sys_unstable_apis"] -rustdocflags = ["--cfg=web_sys_unstable_apis"] \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 5c557e6..1c7a7c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,3 @@ ssr = [] [package.metadata.docs.rs] all-features = true -rustdoc-args = ["--cfg=web_sys_unstable_apis"] -rustc-args = ["--cfg=web_sys_unstable_apis"] - diff --git a/examples/.cargo/config.toml b/examples/.cargo/config.toml deleted file mode 100644 index c87f326..0000000 --- a/examples/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -rustflags = ["--cfg=web_sys_unstable_apis", "--cfg=has_std"] 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/src/lib.rs b/src/lib.rs index a9700c1..811846c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,6 @@ // #![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; @@ -12,14 +10,6 @@ pub mod math; pub mod storage; pub mod utils; -cfg_if! { if #[cfg(web_sys_unstable_apis)] { - mod use_element_size; - mod use_resize_observer; - - pub use use_element_size::*; - pub use use_resize_observer::*; -}} - mod is_err; mod is_none; mod is_ok; @@ -38,6 +28,7 @@ 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; @@ -54,6 +45,7 @@ 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; @@ -90,6 +82,7 @@ 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::*; @@ -106,6 +99,7 @@ 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::*; diff --git a/src/use_element_size.rs b/src/use_element_size.rs index 8831505..273af57 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); /// diff --git a/src/use_resize_observer.rs b/src/use_resize_observer.rs index 2e35e85..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( diff --git a/template/.ffizer.yaml b/template/.ffizer.yaml index cafee3e..58c6216 100644 --- a/template/.ffizer.yaml +++ b/template/.ffizer.yaml @@ -3,10 +3,6 @@ variables: ask: Name of the function - name: category ask: Documentation category (lower case) - - name: unstable_apis - default_value: "false" - ask: Does the function require `--cfg=web_sys_unstable_apis` to be activated? - select_in_values: ["true", "false"] - name: module default_value: "" ask: Module [optional] diff --git a/template/src/{{ module }}/{{ function_name }}.ffizer.hbs.rs b/template/src/{{ module }}/{{ function_name }}.ffizer.hbs.rs index 0edb6ba..3217414 100644 --- a/template/src/{{ module }}/{{ function_name }}.ffizer.hbs.rs +++ b/template/src/{{ module }}/{{ function_name }}.ffizer.hbs.rs @@ -3,9 +3,6 @@ use leptos::*; ///{{#if (eq unstable_apis "true")}} /// -/// > 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).{{/if}} -/// /// ## Demo /// /// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/{{ function_name }}) From e242834b665f69c47a2fe6d76c010a668e8b74af Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Wed, 8 Nov 2023 11:38:07 +0000 Subject: [PATCH 55/90] Specify minimum wasm-bindgen version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1c7a7c1..c558d1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ num = { version = "0.4", optional = true } paste = "1" serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } -wasm-bindgen = "0.2" +wasm-bindgen = "0.2.87" wasm-bindgen-futures = "0.4" [dependencies.web-sys] From 9c3970a95025506eeb1c9e7e42c86bdac9da8e64 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Thu, 9 Nov 2023 23:26:47 +0000 Subject: [PATCH 56/90] Fixed SSR of use_timestamp, use_raf_fn and use_idle Fixes #45 --- CHANGELOG.md | 9 +++ Cargo.toml | 2 +- examples/ssr/src/app.rs | 7 +- src/core/datetime.rs | 16 +++++ src/core/element_maybe_signal.rs | 2 + src/core/mod.rs | 2 + src/use_idle.rs | 106 +++++++++++++++++++------------ src/use_raf_fn.rs | 44 ++++++++----- src/use_timestamp.rs | 8 ++- src/utils/filters/throttle.rs | 10 +-- 10 files changed, 136 insertions(+), 70 deletions(-) create mode 100644 src/core/datetime.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 82633c2..e1c38c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.2] - 2023-11-09 + +### Fixes 🍕 + +- Fixed SSR for + - use_timestamp + - use_raf_fn + - use_idle + ## [0.8.1] - 2023-10-28 ### Fixes 🍕 diff --git a/Cargo.toml b/Cargo.toml index 5c557e6..7658bcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos-use" -version = "0.8.1" +version = "0.8.2" edition = "2021" authors = ["Marc-Stefan Cassola"] categories = ["gui", "web-programming"] diff --git a/examples/ssr/src/app.rs b/examples/ssr/src/app.rs index 20f3b91..2d5da01 100644 --- a/examples/ssr/src/app.rs +++ b/examples/ssr/src/app.rs @@ -5,8 +5,8 @@ use leptos_meta::*; use leptos_router::*; use leptos_use::storage::use_local_storage; use leptos_use::{ - use_color_mode, use_debounce_fn, use_event_listener, use_intl_number_format, use_window, - ColorMode, UseColorModeReturn, UseIntlNumberFormatOptions, + use_color_mode, use_debounce_fn, use_event_listener, use_intl_number_format, use_timestamp, + use_window, ColorMode, UseColorModeReturn, UseIntlNumberFormatOptions, }; #[component] @@ -65,6 +65,8 @@ fn HomePage() -> impl IntoView { let UseColorModeReturn { mode, set_mode, .. } = use_color_mode(); + let timestamp = use_timestamp(); + view! {

Leptos-Use SSR Example

@@ -75,5 +77,6 @@ fn HomePage() -> impl IntoView { +

{timestamp}

} } 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/element_maybe_signal.rs b/src/core/element_maybe_signal.rs index 20dd423..a100b7e 100644 --- a/src/core/element_maybe_signal.rs +++ b/src/core/element_maybe_signal.rs @@ -179,6 +179,7 @@ where { fn from(target: &'a str) -> Self { cfg_if! { if #[cfg(feature = "ssr")] { + let _ = target; Self::Static(None) } else { Self::Static(document().query_selector(target).unwrap_or_default()) @@ -201,6 +202,7 @@ where { fn from(signal: Signal) -> Self { cfg_if! { if #[cfg(feature = "ssr")] { + let _ = signal; Self::Dynamic(Signal::derive(|| None)) } else { Self::Dynamic( diff --git a/src/core/mod.rs b/src/core/mod.rs index 25d7d04..9abe354 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,4 +1,5 @@ mod connection_ready_state; +mod datetime; mod direction; mod element_maybe_signal; mod elements_maybe_signal; @@ -10,6 +11,7 @@ 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::*; diff --git a/src/use_idle.rs b/src/use_idle.rs index 247c867..8a9cc96 100644 --- a/src/use_idle.rs +++ b/src/use_idle.rs @@ -1,8 +1,10 @@ +use crate::core::now; 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 cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::ev::{visibilitychange, Custom}; use leptos::leptos_dom::helpers::TimeoutHandle; @@ -54,6 +56,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 +85,65 @@ 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; + } else { + let reset = { + let timer = Cell::new(None::); - move || { - set_idle.set(false); - if let Some(timer) = timer.take() { - timer.clear(); + move || { + set_idle.set(false); + if let Some(timer) = timer.take() { + timer.clear(); + } + timer.replace( + set_timeout_with_handle(move || set_idle.set(true), Duration::from_millis(timeout)) + .ok(), + ); } - 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_raf_fn.rs b/src/use_raf_fn.rs index c44a00a..b18503c 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 = { diff --git a/src/use_timestamp.rs b/src/use_timestamp.rs index 1718e4f..cdc2576 100644 --- a/src/use_timestamp.rs +++ b/src/use_timestamp.rs @@ -1,3 +1,4 @@ +use crate::core::now; use crate::utils::Pausable; use crate::{ use_interval_fn_with_options, use_raf_fn_with_options, UseIntervalFnOptions, UseRafFnOptions, @@ -47,7 +48,8 @@ use std::rc::Rc; /// /// ## Server-Side Rendering /// -/// On the server this function will simply be ignored. +/// On the server this function will return a signal with the milliseconds since the Unix epoch. +/// But the signal will never update (as there's no `request_animation_frame` on the server). pub fn use_timestamp() -> Signal { use_timestamp_with_controls().timestamp } @@ -71,10 +73,10 @@ pub fn use_timestamp_with_controls_and_options(options: UseTimestampOptions) -> callback, } = options; - let (ts, set_ts) = create_signal(js_sys::Date::now() + offset); + let (ts, set_ts) = create_signal(now() + offset); let update = move || { - set_ts.set(js_sys::Date::now() + offset); + set_ts.set(now() + offset); }; let cb = { diff --git a/src/utils/filters/throttle.rs b/src/utils/filters/throttle.rs index caaa15e..ceda428 100644 --- a/src/utils/filters/throttle.rs +++ b/src/utils/filters/throttle.rs @@ -1,8 +1,8 @@ #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] +use crate::core::now; use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; -use js_sys::Date; use leptos::leptos_dom::helpers::TimeoutHandle; use leptos::{set_timeout_with_handle, MaybeSignal, SignalGetUntracked}; use std::cell::{Cell, RefCell}; @@ -51,7 +51,7 @@ where move |mut _invoke: Rc R>| { let duration = ms.get_untracked(); - let elapsed = Date::now() - last_exec.get(); + let elapsed = now() - last_exec.get(); let last_return_val = Rc::clone(&last_return_value); let invoke = move || { @@ -65,13 +65,13 @@ where clear(); if duration <= 0.0 { - last_exec.set(Date::now()); + last_exec.set(now()); invoke(); return Rc::clone(&last_return_value); } if elapsed > duration && (options.leading || !is_leading.get()) { - last_exec.set(Date::now()); + last_exec.set(now()); invoke(); } else if options.trailing { cfg_if! { if #[cfg(not(feature = "ssr"))] { @@ -80,7 +80,7 @@ where timer.set( set_timeout_with_handle( move || { - last_exec.set(Date::now()); + last_exec.set(now()); is_leading.set(true); invoke(); clear(); From f918da96884618cdedf2ed2602e49ddde4c06ba6 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 10 Nov 2023 17:14:14 +0000 Subject: [PATCH 57/90] Revert remove use of --cfg=web_sys_unstable_apis for templates --- template/.ffizer.yaml | 4 ++++ template/src/{{ module }}/{{ function_name }}.ffizer.hbs.rs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/template/.ffizer.yaml b/template/.ffizer.yaml index 58c6216..cafee3e 100644 --- a/template/.ffizer.yaml +++ b/template/.ffizer.yaml @@ -3,6 +3,10 @@ variables: ask: Name of the function - name: category ask: Documentation category (lower case) + - name: unstable_apis + default_value: "false" + ask: Does the function require `--cfg=web_sys_unstable_apis` to be activated? + select_in_values: ["true", "false"] - name: module default_value: "" ask: Module [optional] diff --git a/template/src/{{ module }}/{{ function_name }}.ffizer.hbs.rs b/template/src/{{ module }}/{{ function_name }}.ffizer.hbs.rs index 3217414..0edb6ba 100644 --- a/template/src/{{ module }}/{{ function_name }}.ffizer.hbs.rs +++ b/template/src/{{ module }}/{{ function_name }}.ffizer.hbs.rs @@ -3,6 +3,9 @@ use leptos::*; ///{{#if (eq unstable_apis "true")}} /// +/// > 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).{{/if}} +/// /// ## Demo /// /// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/{{ function_name }}) From 1d08beaa3e454840fa4a71942dedb4708ed6fb50 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Fri, 10 Nov 2023 18:13:56 +0000 Subject: [PATCH 58/90] Problem: clippy says Default impl can be derived --- src/use_element_size.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/use_element_size.rs b/src/use_element_size.rs index 273af57..bc6a226 100644 --- a/src/use_element_size.rs +++ b/src/use_element_size.rs @@ -172,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 @@ -184,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. From 5b056e8d19744a82659d7e8fb4946c8764092e2b Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 10:43:32 +0000 Subject: [PATCH 59/90] Problem: CI references storage feature. Replace with serde,prost --- .fleet/run.json | 6 +++--- .github/workflows/ci.yml | 4 ++-- .github/workflows/tests.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.fleet/run.json b/.fleet/run.json index 4e4ffb5..a849842 100644 --- a/.fleet/run.json +++ b/.fleet/run.json @@ -3,14 +3,14 @@ { "type": "cargo", "name": "Tests", - "cargoArgs": ["test", "--features", "math,storage,docs"], + "cargoArgs": ["test", "--features", "math,prost,serde,docs"], }, { "type": "cargo", "name": "Clippy", - "cargoArgs": ["+nightly", "clippy", "--features", "math,storage,docs", "--tests", "--", "-D", "warnings"], + "cargoArgs": ["+nightly", "clippy", "--features", "math,prost,serde,docs", "--tests", "--", "-D", "warnings"], "workingDir": "./", }, ] -} \ No newline at end of file +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d883bd8..6ad089d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - name: Check formatting run: cargo fmt --check - name: Clippy - run: cargo clippy --features storage,docs,math --tests -- -D warnings + run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings - name: Run tests run: cargo test --all-features @@ -110,4 +110,4 @@ jobs: # - name: Publish to Coveralls # uses: coverallsapp/github-action@master # with: -# github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file +# github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 70a765e..f5500fb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,6 +23,6 @@ jobs: - name: Check formatting run: cargo fmt --check - name: Clippy - run: cargo clippy --features storage,docs,math --tests -- -D warnings + run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings - name: Run tests run: cargo test --all-features From 2976d4c03374dbab023d311101af8967710e7ac3 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 10:51:23 +0000 Subject: [PATCH 60/90] Fix clippy lints --- src/storage/use_storage.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index f640d25..466a38a 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -73,7 +73,7 @@ pub enum UseStorageError { /// See [`use_storage_with_options`] for more details on how to use. pub fn use_local_storage( key: impl AsRef, -) -> (Signal, WriteSignal, impl Fn() -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + Default + PartialEq, C: Codec + Default, @@ -89,7 +89,7 @@ where pub fn use_local_storage_with_options( key: impl AsRef, options: UseStorageOptions, -) -> (Signal, WriteSignal, impl Fn() -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + PartialEq, C: Codec, @@ -106,7 +106,7 @@ where /// See [`use_storage_with_options`] for more details on how to use. pub fn use_session_storage( key: impl AsRef, -) -> (Signal, WriteSignal, impl Fn() -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + Default + PartialEq, C: Codec + Default, @@ -122,7 +122,7 @@ where pub fn use_session_storage_with_options( key: impl AsRef, options: UseStorageOptions, -) -> (Signal, WriteSignal, impl Fn() -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + PartialEq, C: Codec, @@ -193,7 +193,7 @@ pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, options: UseStorageOptions, -) -> (Signal, WriteSignal, impl Fn() -> () + Clone) +) -> (Signal, WriteSignal, impl Fn() + Clone) where T: Clone + PartialEq, C: Codec, @@ -382,7 +382,7 @@ where } }; - (data.into(), set_data, remove) + (data, set_data, remove) }} } @@ -391,7 +391,7 @@ fn handle_error( on_error: &Rc)>, result: Result>, ) -> Result { - result.or_else(|err| Err((on_error)(err))) + result.map_err(|err| (on_error)(err)) } impl + Default> Default for UseStorageOptions { From 93556a3f8f2fd86b8996c3633c659c842184ed73 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 10:56:46 +0000 Subject: [PATCH 61/90] Fix clippy lint. Remove needless borrow --- src/storage/codec_prost.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/codec_prost.rs b/src/storage/codec_prost.rs index 28986b2..1c90ec8 100644 --- a/src/storage/codec_prost.rs +++ b/src/storage/codec_prost.rs @@ -45,7 +45,7 @@ impl Codec for ProstCodec { fn encode(&self, val: &T) -> Result { let buf = val.encode_to_vec(); - Ok(base64::engine::general_purpose::STANDARD.encode(&buf)) + Ok(base64::engine::general_purpose::STANDARD.encode(buf)) } fn decode(&self, str: String) -> Result { From 94841e4bb5b93a81f2f83b05577ad6316c831073 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 11:05:29 +0000 Subject: [PATCH 62/90] Document fields on UseStorageOptions --- src/storage/use_storage.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 466a38a..6455c4b 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -38,10 +38,15 @@ pub trait Codec: Clone + 'static { /// 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, } From 3867a4bd1dcfa6a819b6bc99700837c8ce501477 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 12:41:04 +0000 Subject: [PATCH 63/90] Problem: mdbook use_service_worker script is not portable --- examples/use_service_worker/Trunk.toml | 2 +- examples/use_service_worker/post_build.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/use_service_worker/Trunk.toml b/examples/use_service_worker/Trunk.toml index 241fda5..341a2b1 100644 --- a/examples/use_service_worker/Trunk.toml +++ b/examples/use_service_worker/Trunk.toml @@ -4,4 +4,4 @@ public_url = "/demo/" [[hooks]] stage = "post_build" command = "bash" -command_arguments = ["-c", "./post_build.sh"] +command_arguments = ["post_build.sh"] diff --git a/examples/use_service_worker/post_build.sh b/examples/use_service_worker/post_build.sh index 202a47b..e6ebf5a 100755 --- a/examples/use_service_worker/post_build.sh +++ b/examples/use_service_worker/post_build.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e appName="use_service_worker" From cf33e317a663698672b02ad742ceb7bcd9af1ce8 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 12:56:32 +0000 Subject: [PATCH 64/90] Rename use_storage.rs to align with mdbook reference --- src/storage/{use_storage.rs => use_storage_with_options.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/storage/{use_storage.rs => use_storage_with_options.rs} (100%) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage_with_options.rs similarity index 100% rename from src/storage/use_storage.rs rename to src/storage/use_storage_with_options.rs From 99803a9bcb779405915939d4799d0947e087c45c Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 13:15:13 +0000 Subject: [PATCH 65/90] Organise storage docs for mdbook --- src/storage/mod.rs | 8 +- src/storage/use_local_storage.rs | 36 +++++ src/storage/use_session_storage.rs | 36 +++++ src/storage/use_storage_with_options.rs | 180 ++++++++---------------- 4 files changed, 135 insertions(+), 125 deletions(-) create mode 100644 src/storage/use_local_storage.rs create mode 100644 src/storage/use_session_storage.rs diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 7183c63..46da6cb 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -3,7 +3,9 @@ mod codec_json; #[cfg(feature = "prost")] mod codec_prost; mod codec_string; -mod use_storage; +mod use_local_storage; +mod use_session_storage; +mod use_storage_with_options; pub use crate::core::StorageType; #[cfg(feature = "serde")] @@ -11,4 +13,6 @@ pub use codec_json::*; #[cfg(feature = "prost")] pub use codec_prost::*; pub use codec_string::*; -pub use use_storage::*; +pub use use_local_storage::*; +pub use use_session_storage::*; +pub use use_storage_with_options::*; diff --git a/src/storage/use_local_storage.rs b/src/storage/use_local_storage.rs new file mode 100644 index 0000000..44725ce --- /dev/null +++ b/src/storage/use_local_storage.rs @@ -0,0 +1,36 @@ +use super::{use_storage_with_options, Codec, StorageType, UseStorageOptions}; +use leptos::signal_prelude::*; + +/// 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_with_options`] 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 new file mode 100644 index 0000000..1d7286d --- /dev/null +++ b/src/storage/use_session_storage.rs @@ -0,0 +1,36 @@ +use super::{use_storage_with_options, Codec, StorageType, UseStorageOptions}; +use leptos::signal_prelude::*; + +/// 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_with_options`] 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_with_options.rs b/src/storage/use_storage_with_options.rs index 6455c4b..076f36d 100644 --- a/src/storage/use_storage_with_options.rs +++ b/src/storage/use_storage_with_options.rs @@ -12,129 +12,6 @@ use wasm_bindgen::JsValue; const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; -/// 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; -} - -/// 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, -} - -/// 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), -} - -/// 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. -/// -/// See [`use_storage_with_options`] 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) -} - -/// 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. -/// -/// See [`use_storage_with_options`] 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) -} - /// Reactive [Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). /// /// * [See a demo](https://leptos-use.rs/storage/use_storage.html) @@ -391,6 +268,63 @@ where }} } +/// 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), +} + +/// 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, +} + +/// 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; +} + /// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling. fn handle_error( on_error: &Rc)>, From 9306e0299c85e864b9870991c4a79d739483f50c Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 13:15:34 +0000 Subject: [PATCH 66/90] Remove storage flag from mdbook --- docs/book/src/storage/use_local_storage.md | 2 +- docs/book/src/storage/use_session_storage.md | 2 +- docs/book/src/storage/use_storage_with_options.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/book/src/storage/use_local_storage.md b/docs/book/src/storage/use_local_storage.md index d39b415..3256f85 100644 --- a/docs/book/src/storage/use_local_storage.md +++ b/docs/book/src/storage/use_local_storage.md @@ -1,3 +1,3 @@ # use_local_storage - + diff --git a/docs/book/src/storage/use_session_storage.md b/docs/book/src/storage/use_session_storage.md index f04e6e5..03ddbab 100644 --- a/docs/book/src/storage/use_session_storage.md +++ b/docs/book/src/storage/use_session_storage.md @@ -1,3 +1,3 @@ # use_session_storage - + diff --git a/docs/book/src/storage/use_storage_with_options.md b/docs/book/src/storage/use_storage_with_options.md index 26c94e7..3fe134f 100644 --- a/docs/book/src/storage/use_storage_with_options.md +++ b/docs/book/src/storage/use_storage_with_options.md @@ -1,3 +1,3 @@ # use_storage_with_options - + From e8c6540369349c7d5b6b2d824071921934b865b5 Mon Sep 17 00:00:00 2001 From: Joshua McQuistan Date: Sat, 11 Nov 2023 13:16:20 +0000 Subject: [PATCH 67/90] Remove @ prefix on mdbook storage reference --- docs/book/src/SUMMARY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 8a56599..016228b 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -6,7 +6,7 @@ [Changelog](changelog.md) [Functions](functions.md) -# @Storage +# Storage - [use_local_storage](storage/use_local_storage.md) - [use_session_storage](storage/use_session_storage.md) From b26459986edb4cded3c61b156b255310da3203c7 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Sun, 12 Nov 2023 22:50:59 +0000 Subject: [PATCH 68/90] brought back use_storage --- src/storage/codec_json.rs | 6 +++--- src/storage/codec_prost.rs | 2 +- src/storage/codec_string.rs | 2 +- src/storage/mod.rs | 4 ++-- src/storage/use_local_storage.rs | 4 ++-- src/storage/use_session_storage.rs | 4 ++-- ...use_storage_with_options.rs => use_storage.rs} | 15 ++++++++++++++- src/use_color_mode.rs | 2 +- 8 files changed, 26 insertions(+), 13 deletions(-) rename src/storage/{use_storage_with_options.rs => use_storage.rs} (97%) diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs index c12d3a7..5a834a1 100644 --- a/src/storage/codec_json.rs +++ b/src/storage/codec_json.rs @@ -5,7 +5,7 @@ use super::Codec; /// ## Example /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, JsonCodec}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, JsonCodec}; /// # use serde::{Deserialize, Serialize}; /// # /// # pub fn Demo() -> impl IntoView { @@ -34,7 +34,7 @@ use super::Codec; /// /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, Codec, JsonCodec}; +/// # 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 { @@ -75,7 +75,7 @@ use super::Codec; /// /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, Codec, JsonCodec}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, Codec, JsonCodec}; /// # use serde::{Deserialize, Serialize}; /// # use serde_json::json; /// # diff --git a/src/storage/codec_prost.rs b/src/storage/codec_prost.rs index 1c90ec8..e833410 100644 --- a/src/storage/codec_prost.rs +++ b/src/storage/codec_prost.rs @@ -11,7 +11,7 @@ use thiserror::Error; /// ## Example /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, ProstCodec}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, ProstCodec}; /// # /// # pub fn Demo() -> impl IntoView { /// // Primitive types: diff --git a/src/storage/codec_string.rs b/src/storage/codec_string.rs index 2c16f34..1cae942 100644 --- a/src/storage/codec_string.rs +++ b/src/storage/codec_string.rs @@ -8,7 +8,7 @@ use std::str::FromStr; /// ## Example /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec}; +/// # 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"); diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 46da6cb..c5cd494 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -5,7 +5,7 @@ mod codec_prost; mod codec_string; mod use_local_storage; mod use_session_storage; -mod use_storage_with_options; +mod use_storage; pub use crate::core::StorageType; #[cfg(feature = "serde")] @@ -15,4 +15,4 @@ pub use codec_prost::*; pub use codec_string::*; pub use use_local_storage::*; pub use use_session_storage::*; -pub use use_storage_with_options::*; +pub use use_storage::*; diff --git a/src/storage/use_local_storage.rs b/src/storage/use_local_storage.rs index 44725ce..73d864d 100644 --- a/src/storage/use_local_storage.rs +++ b/src/storage/use_local_storage.rs @@ -1,4 +1,4 @@ -use super::{use_storage_with_options, Codec, StorageType, UseStorageOptions}; +use super::{use_storage, Codec, StorageType, UseStorageOptions}; use leptos::signal_prelude::*; /// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). @@ -8,7 +8,7 @@ use leptos::signal_prelude::*; /// This is contrast to [`use_session_storage`] which clears data when the page session ends and is not shared. /// /// ## Usage -/// See [`use_storage_with_options`] for more details on how to use. +/// See [`use_storage`] for more details on how to use. pub fn use_local_storage( key: impl AsRef, ) -> (Signal, WriteSignal, impl Fn() + Clone) diff --git a/src/storage/use_session_storage.rs b/src/storage/use_session_storage.rs index 1d7286d..ae6e1eb 100644 --- a/src/storage/use_session_storage.rs +++ b/src/storage/use_session_storage.rs @@ -1,4 +1,4 @@ -use super::{use_storage_with_options, Codec, StorageType, UseStorageOptions}; +use super::{use_storage, Codec, StorageType, UseStorageOptions}; use leptos::signal_prelude::*; /// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). @@ -8,7 +8,7 @@ use leptos::signal_prelude::*; /// 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_with_options`] for more details on how to use. +/// See [`use_storage`] for more details on how to use. pub fn use_session_storage( key: impl AsRef, ) -> (Signal, WriteSignal, impl Fn() + Clone) diff --git a/src/storage/use_storage_with_options.rs b/src/storage/use_storage.rs similarity index 97% rename from src/storage/use_storage_with_options.rs rename to src/storage/use_storage.rs index 076f36d..6456f9f 100644 --- a/src/storage/use_storage_with_options.rs +++ b/src/storage/use_storage.rs @@ -29,7 +29,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; /// # use serde::{Deserialize, Serialize}; /// # /// # pub fn Demo() -> impl IntoView { @@ -71,6 +71,19 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// } /// } /// ``` +#[inline(always)] +pub fn use_storage( + storage_type: StorageType, + key: impl AsRef, +) -> (Signal, WriteSignal, impl Fn() + Clone) +where + T: Clone + PartialEq, + C: Codec, +{ + use_storage_with_options(storage_type, key, UseStorageOptions::default()) +} + +/// Version of [`use_storage`] that accepts [`UseStorageOptions`]. pub fn use_storage_with_options( storage_type: StorageType, key: impl AsRef, diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index 5f2b781..a9dd6fb 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -1,5 +1,5 @@ use crate::core::{ElementMaybeSignal, MaybeRwSignal}; -use crate::storage::{use_storage_with_options, StringCodec, UseStorageOptions}; +use crate::storage::{use_storage, StringCodec, UseStorageOptions}; use std::fmt::{Display, Formatter}; use std::str::FromStr; From 8199d19e924911c76868906db725df816be1668b Mon Sep 17 00:00:00 2001 From: Maccesch Date: Sun, 12 Nov 2023 23:02:36 +0000 Subject: [PATCH 69/90] brought back use_storage in book --- docs/book/src/SUMMARY.md | 2 +- .../storage/{use_storage_with_options.md => use_storage.md} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename docs/book/src/storage/{use_storage_with_options.md => use_storage.md} (58%) diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 016228b..35658f4 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -10,7 +10,7 @@ - [use_local_storage](storage/use_local_storage.md) - [use_session_storage](storage/use_session_storage.md) -- [use_storage_with_options](storage/use_storage_with_options.md) +- [use_storage](storage/use_storage.md) # Elements diff --git a/docs/book/src/storage/use_storage_with_options.md b/docs/book/src/storage/use_storage.md similarity index 58% rename from docs/book/src/storage/use_storage_with_options.md rename to docs/book/src/storage/use_storage.md index 3fe134f..316df04 100644 --- a/docs/book/src/storage/use_storage_with_options.md +++ b/docs/book/src/storage/use_storage.md @@ -1,3 +1,3 @@ -# use_storage_with_options +# use_storage - + From 796ea23ddfaa78e82b5e4cdc401b401b7e295cc5 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Sun, 12 Nov 2023 23:07:10 +0000 Subject: [PATCH 70/90] restored use_storage demo --- src/storage/use_storage.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index 6456f9f..bce2f10 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -14,8 +14,9 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// Reactive [Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). /// -/// * [See a demo](https://leptos-use.rs/storage/use_storage.html) -/// * [See a full example](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_storage) +/// ## Demo +/// +/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_storage) /// /// ## Usage /// From 28ab799156c4c3b7b93d8df20b9b1f067b68e364 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Mon, 13 Nov 2023 00:20:58 +0000 Subject: [PATCH 71/90] removed warnings and fixed examples --- CHANGELOG.md | 15 +++++++++++++++ examples/ssr/Cargo.toml | 2 +- examples/ssr/src/app.rs | 4 ++-- src/storage/codec_json.rs | 4 ++-- src/storage/codec_prost.rs | 4 ++-- src/storage/codec_string.rs | 4 ++-- src/storage/use_local_storage.rs | 2 +- src/storage/use_session_storage.rs | 2 +- src/storage/use_storage.rs | 28 ++++++++++++++++++---------- src/use_color_mode.rs | 2 +- src/use_idle.rs | 20 +++++++++++--------- 11 files changed, 56 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1c38c4..fed0397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Breaking Changes 🛠 + +- (@feral-dot-io) The use `use__storage` functions have been rewritten to use `Codec`s instead of always requiring `serde`. + - This also removes the feature `storage` + - By default the `StringCodec` is used which relies on types implementing `FromString + ToString` + - If you want to use `JsonCodec` you have to enable the feature `serde` + - If you want to use `ProstCodec` (new!) you have to enable the feature `prost`. +- (@feral-dot-io) The Rust flag `--cfg=web_sys_unstable_apis` is not needed anymore since relevant `web_sys` APIs are now stable. + This affects in particular + - `use_element_size` + - `use_resize_observer` + + ## [0.8.2] - 2023-11-09 ### Fixes 🍕 diff --git a/examples/ssr/Cargo.toml b/examples/ssr/Cargo.toml index 782e066..d37a754 100644 --- a/examples/ssr/Cargo.toml +++ b/examples/ssr/Cargo.toml @@ -21,7 +21,7 @@ simple_logger = "4" tokio = { version = "1.25.0", optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.4.3", features = ["fs"], optional = true } -wasm-bindgen = "=0.2.87" +wasm-bindgen = "0.2.88" thiserror = "1.0.38" tracing = { version = "0.1.37", optional = true } http = "0.2.8" diff --git a/examples/ssr/src/app.rs b/examples/ssr/src/app.rs index 2d5da01..cdfe5e3 100644 --- a/examples/ssr/src/app.rs +++ b/examples/ssr/src/app.rs @@ -3,7 +3,7 @@ use leptos::ev::{keypress, KeyboardEvent}; use leptos::*; use leptos_meta::*; use leptos_router::*; -use leptos_use::storage::use_local_storage; +use leptos_use::storage::{use_local_storage, StringCodec}; use leptos_use::{ use_color_mode, use_debounce_fn, use_event_listener, use_intl_number_format, use_timestamp, use_window, ColorMode, UseColorModeReturn, UseIntlNumberFormatOptions, @@ -37,7 +37,7 @@ pub fn App() -> impl IntoView { #[component] fn HomePage() -> impl IntoView { // Creates a reactive value to update the button - let (count, set_count, _) = use_local_storage("count-state", 0); + let (count, set_count, _) = use_local_storage::("count-state"); let on_click = move |_| set_count.update(|count| *count += 1); let nf = use_intl_number_format( diff --git a/src/storage/codec_json.rs b/src/storage/codec_json.rs index 5a834a1..1a6a3a9 100644 --- a/src/storage/codec_json.rs +++ b/src/storage/codec_json.rs @@ -114,7 +114,7 @@ use super::Codec; /// # } /// ``` #[derive(Clone, Default, PartialEq)] -pub struct JsonCodec(); +pub struct JsonCodec; impl Codec for JsonCodec { type Error = serde_json::Error; @@ -143,7 +143,7 @@ mod tests { s: String::from("party time 🎉"), i: 42, }; - let codec = JsonCodec(); + 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 index e833410..2311e28 100644 --- a/src/storage/codec_prost.rs +++ b/src/storage/codec_prost.rs @@ -30,7 +30,7 @@ use thiserror::Error; /// /// 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(); +pub struct ProstCodec; #[derive(Error, Debug, PartialEq)] pub enum ProstCodecError { @@ -73,7 +73,7 @@ mod tests { s: String::from("party time 🎉"), i: 42, }; - let codec = ProstCodec(); + 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 index 1cae942..c7a8049 100644 --- a/src/storage/codec_string.rs +++ b/src/storage/codec_string.rs @@ -16,7 +16,7 @@ use std::str::FromStr; /// # } /// ``` #[derive(Clone, Default, PartialEq)] -pub struct StringCodec(); +pub struct StringCodec; impl Codec for StringCodec { type Error = T::Err; @@ -37,7 +37,7 @@ mod tests { #[test] fn test_string_codec() { let s = String::from("party time 🎉"); - let codec = StringCodec(); + let codec = StringCodec; assert_eq!(codec.encode(&s), Ok(s.clone())); assert_eq!(codec.decode(s.clone()), Ok(s)); } diff --git a/src/storage/use_local_storage.rs b/src/storage/use_local_storage.rs index 73d864d..4479a4a 100644 --- a/src/storage/use_local_storage.rs +++ b/src/storage/use_local_storage.rs @@ -1,4 +1,4 @@ -use super::{use_storage, Codec, StorageType, UseStorageOptions}; +use super::{use_storage_with_options, Codec, StorageType, UseStorageOptions}; use leptos::signal_prelude::*; /// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). diff --git a/src/storage/use_session_storage.rs b/src/storage/use_session_storage.rs index ae6e1eb..127e457 100644 --- a/src/storage/use_session_storage.rs +++ b/src/storage/use_session_storage.rs @@ -1,4 +1,4 @@ -use super::{use_storage, Codec, StorageType, UseStorageOptions}; +use super::{use_storage_with_options, Codec, StorageType, UseStorageOptions}; use leptos::signal_prelude::*; /// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs index bce2f10..3c66fce 100644 --- a/src/storage/use_storage.rs +++ b/src/storage/use_storage.rs @@ -1,8 +1,7 @@ +use crate::storage::StringCodec; use crate::{ core::{MaybeRwSignal, StorageType}, - use_event_listener, use_window, utils::FilterOptions, - watch_with_options, WatchOptions, }; use cfg_if::cfg_if; use leptos::*; @@ -30,7 +29,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// /// ``` /// # use leptos::*; -/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; +/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage_with_options, UseStorageOptions, StringCodec, JsonCodec, ProstCodec}; /// # use serde::{Deserialize, Serialize}; /// # /// # pub fn Demo() -> impl IntoView { @@ -73,15 +72,11 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage"; /// } /// ``` #[inline(always)] -pub fn use_storage( +pub fn use_storage( storage_type: StorageType, key: impl AsRef, -) -> (Signal, WriteSignal, impl Fn() + Clone) -where - T: Clone + PartialEq, - C: Codec, -{ - use_storage_with_options(storage_type, key, UseStorageOptions::default()) +) -> (Signal, WriteSignal, impl Fn() + Clone) { + use_storage_with_options::(storage_type, key, UseStorageOptions::default()) } /// Version of [`use_storage`] that accepts [`UseStorageOptions`]. @@ -106,11 +101,23 @@ where let default = data.get_untracked(); cfg_if! { if #[cfg(feature = "ssr")] { + 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 { + use crate::{use_event_listener, use_window, watch_with_options, WatchOptions}; + // Get storage API let storage = storage_type .into_storage() @@ -340,6 +347,7 @@ pub trait Codec: Clone + 'static { } /// 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>, diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index a9dd6fb..5f2b781 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -1,5 +1,5 @@ use crate::core::{ElementMaybeSignal, MaybeRwSignal}; -use crate::storage::{use_storage, StringCodec, UseStorageOptions}; +use crate::storage::{use_storage_with_options, StringCodec, UseStorageOptions}; use std::fmt::{Display, Formatter}; use std::str::FromStr; diff --git a/src/use_idle.rs b/src/use_idle.rs index 8a9cc96..1819929 100644 --- a/src/use_idle.rs +++ b/src/use_idle.rs @@ -1,16 +1,9 @@ use crate::core::now; -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::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; /// /// @@ -93,7 +86,16 @@ pub fn use_idle_with_options( 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::time::Duration; + let reset = { let timer = Cell::new(None::); From 4a4feefbfcd1c1036bbb38b2c18943364180b150 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 15 Nov 2023 23:19:11 +0000 Subject: [PATCH 72/90] removed warnings because of get/get_untracked --- src/use_raf_fn.rs | 4 ++-- src/use_timestamp.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/use_raf_fn.rs b/src/use_raf_fn.rs index b18503c..27c7389 100644 --- a/src/use_raf_fn.rs +++ b/src/use_raf_fn.rs @@ -89,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; } @@ -111,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_timestamp.rs b/src/use_timestamp.rs index cdc2576..27024b0 100644 --- a/src/use_timestamp.rs +++ b/src/use_timestamp.rs @@ -84,7 +84,7 @@ pub fn use_timestamp_with_controls_and_options(options: UseTimestampOptions) -> move || { update(); - callback(ts.get()); + callback(ts.get_untracked()); } }; From 09182c5ee966be801f328a8651a0b13a447094c3 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 15 Nov 2023 23:23:06 +0000 Subject: [PATCH 73/90] removed warnings because of get/get_untracked Fixes #52 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fed0397..42bf32b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `use_element_size` - `use_resize_observer` +### Fixes 🍕 + +- `use_raf_fn` and `use_timestamp` no longer spam warnings because of `get`ting signals outside of reactive contexts. ## [0.8.2] - 2023-11-09 From 2d65c60bf1ffc912af51f259b8302c277ee16401 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 15 Nov 2023 23:45:51 +0000 Subject: [PATCH 74/90] removed double call of infinite scroll callback Fixes #50 --- CHANGELOG.md | 1 + Cargo.toml | 4 ++-- src/use_infinite_scroll.rs | 8 +++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42bf32b..93fee73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixes 🍕 - `use_raf_fn` and `use_timestamp` no longer spam warnings because of `get`ting signals outside of reactive contexts. +- `use_infinite_scroll` no longer calls the callback twice for the same event ## [0.8.2] - 2023-11-09 diff --git a/Cargo.toml b/Cargo.toml index 03e168e..4c31f33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,11 +27,11 @@ prost = { version = "0.12", optional = true } serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } thiserror = "1.0" -wasm-bindgen = "0.2.87" +wasm-bindgen = "0.2.88" wasm-bindgen-futures = "0.4" [dependencies.web-sys] -version = "0.3" +version = "0.3.65" features = [ "AddEventListenerOptions", "BinaryType", diff --git a/src/use_infinite_scroll.rs b/src/use_infinite_scroll.rs index d02d97d..2b23261 100644 --- a/src/use_infinite_scroll.rs +++ b/src/use_infinite_scroll.rs @@ -186,7 +186,13 @@ where let _ = watch( move || state.arrived_state.get().get_direction(direction), - move |_, _, _| { + 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")() From b7a3ac554b971d7214be206bfbc6474bcae46ed4 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 15 Nov 2023 23:57:17 +0000 Subject: [PATCH 75/90] debounced on_scroll_end in use_scroll now uses try_get_untracked in case the context has been destroyed before it is called. Fixes #51 --- src/use_scroll.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/use_scroll.rs b/src/use_scroll.rs index d25467b..757c6dc 100644 --- a/src/use_scroll.rs +++ b/src/use_scroll.rs @@ -257,7 +257,7 @@ where 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; } @@ -390,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::< _, From ded266356451e71b39dfd70b9f432f21c56b3aaa Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 15 Nov 2023 23:59:17 +0000 Subject: [PATCH 76/90] updated changelog --- CHANGELOG.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93fee73..67e8a12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,36 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking Changes 🛠 -- (@feral-dot-io) The use `use__storage` functions have been rewritten to use `Codec`s instead of always requiring `serde`. - - This also removes the feature `storage` - - By default the `StringCodec` is used which relies on types implementing `FromString + ToString` - - If you want to use `JsonCodec` you have to enable the feature `serde` - - If you want to use `ProstCodec` (new!) you have to enable the feature `prost`. -- (@feral-dot-io) The Rust flag `--cfg=web_sys_unstable_apis` is not needed anymore since relevant `web_sys` APIs are now stable. +- (@feral-dot-io) The use `use__storage` functions have been rewritten to use `Codec`s instead of always + requiring `serde`. + - This also removes the feature `storage` + - By default the `StringCodec` is used which relies on types implementing `FromString + ToString` + - If you want to use `JsonCodec` you have to enable the feature `serde` + - If you want to use `ProstCodec` (new!) you have to enable the feature `prost`. +- (@feral-dot-io) The Rust flag `--cfg=web_sys_unstable_apis` is not needed anymore since relevant `web_sys` APIs are + now stable. This affects in particular - - `use_element_size` - - `use_resize_observer` + - `use_element_size` + - `use_resize_observer` ### Fixes 🍕 - `use_raf_fn` and `use_timestamp` no longer spam warnings because of `get`ting signals outside of reactive contexts. - `use_infinite_scroll` no longer calls the callback twice for the same event +- `use_scroll` now uses `try_get_untracked` in the debounced callback to avoid panics if the context has been destroyed + while the callback was waiting to be called. ## [0.8.2] - 2023-11-09 ### Fixes 🍕 - Fixed SSR for - - use_timestamp - - use_raf_fn - - use_idle + - use_timestamp + - use_raf_fn + - use_idle ## [0.8.1] - 2023-10-28 ### Fixes 🍕 - Using strings for `ElementMaybeSignal` and `ElementsMaybeSignal` is now SSR safe. - - This fixes specifically `use_color_mode` to work on the server. + - This fixes specifically `use_color_mode` to work on the server. ## [0.8.0] - 2023-10-24 @@ -58,10 +62,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixes 🍕 -- Some functions still used `window()` which could lead to panics in SSR. This is now fixed. +- Some functions still used `window()` which could lead to panics in SSR. This is now fixed. Specifically for `use_draggable`. -## [0.7.1] - 2023-10-02 +## [0.7.1] - 2023-10-02 ### New Function 🚀 From 1d50df3edeef208148d155b253af2401b18e65be Mon Sep 17 00:00:00 2001 From: Lukas Potthast Date: Fri, 24 Nov 2023 17:41:25 +0100 Subject: [PATCH 77/90] Remove logging of WS url --- src/use_websocket.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/use_websocket.rs b/src/use_websocket.rs index c50d308..5b81a9e 100644 --- a/src/use_websocket.rs +++ b/src/use_websocket.rs @@ -212,7 +212,6 @@ pub fn use_websocket_with_options( impl Fn(Vec) + Clone, > { let url = normalize_url(url); - logging::log!("{}", url); let UseWebSocketOptions { on_open, on_message, From cd0be5ac62693907444a5b1a152aad1fce85b2b2 Mon Sep 17 00:00:00 2001 From: Sean Aye Date: Fri, 24 Nov 2023 15:06:35 -0500 Subject: [PATCH 78/90] add use_display_media --- .idea/leptos-use.iml | 5 +++- Cargo.toml | 4 +++ docs/book/src/browser/use_display_media.md | 3 ++ examples/Cargo.toml | 1 + examples/use_display_media/Cargo.toml | 16 ++++++++++ examples/use_display_media/Trunk.toml | 2 ++ examples/use_display_media/index.html | 5 ++++ .../use_display_media/rust-toolchain.toml | 2 ++ examples/use_display_media/src/main.rs | 30 +++++++++++++++++++ src/lib.rs | 2 ++ src/use_display_media.rs | 29 ++++++++++++++++++ 11 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 docs/book/src/browser/use_display_media.md create mode 100644 examples/use_display_media/Cargo.toml create mode 100644 examples/use_display_media/Trunk.toml create mode 100644 examples/use_display_media/index.html create mode 100644 examples/use_display_media/rust-toolchain.toml create mode 100644 examples/use_display_media/src/main.rs create mode 100644 src/use_display_media.rs diff --git a/.idea/leptos-use.iml b/.idea/leptos-use.iml index 830c9eb..224ebd7 100644 --- a/.idea/leptos-use.iml +++ b/.idea/leptos-use.iml @@ -12,6 +12,7 @@ + @@ -67,7 +68,9 @@ + + @@ -90,4 +93,4 @@ - \ No newline at end of file + diff --git a/Cargo.toml b/Cargo.toml index 4c31f33..a4bcbda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ cfg-if = "1" default-struct-builder = "0.5" futures-util = "0.3" gloo-timers = { version = "0.3.0", features = ["futures"] } +gloo-utils = { version = "0.2.0"} js-sys = "0.3" lazy_static = "1" leptos = "0.5" @@ -40,6 +41,7 @@ features = [ "CssStyleDeclaration", "CustomEvent", "CustomEventInit", + "DisplayMediaStreamConstraints", "DomRect", "DomRectReadOnly", "DataTransfer", @@ -57,7 +59,9 @@ features = [ "IntersectionObserver", "IntersectionObserverInit", "IntersectionObserverEntry", + "MediaDevices", "MediaQueryList", + "MediaStream", "MouseEvent", "MutationObserver", "MutationObserverInit", diff --git a/docs/book/src/browser/use_display_media.md b/docs/book/src/browser/use_display_media.md new file mode 100644 index 0000000..5d4219f --- /dev/null +++ b/docs/book/src/browser/use_display_media.md @@ -0,0 +1,3 @@ +# use_display_media + + diff --git a/examples/Cargo.toml b/examples/Cargo.toml index e61f920..d9f03b9 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -13,6 +13,7 @@ members = [ "use_css_var", "use_cycle_list", "use_debounce_fn", + "use_display_media", "use_document_visibility", "use_draggable", "use_drop_zone", diff --git a/examples/use_display_media/Cargo.toml b/examples/use_display_media/Cargo.toml new file mode 100644 index 0000000..32014f2 --- /dev/null +++ b/examples/use_display_media/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "use_display_media" +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_display_media/Trunk.toml b/examples/use_display_media/Trunk.toml new file mode 100644 index 0000000..3e4be08 --- /dev/null +++ b/examples/use_display_media/Trunk.toml @@ -0,0 +1,2 @@ +[build] +public_url = "/demo/" \ No newline at end of file diff --git a/examples/use_display_media/index.html b/examples/use_display_media/index.html new file mode 100644 index 0000000..25f83eb --- /dev/null +++ b/examples/use_display_media/index.html @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/use_display_media/rust-toolchain.toml b/examples/use_display_media/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/examples/use_display_media/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/examples/use_display_media/src/main.rs b/examples/use_display_media/src/main.rs new file mode 100644 index 0000000..01cb48e --- /dev/null +++ b/examples/use_display_media/src/main.rs @@ -0,0 +1,30 @@ +use leptos::*; +use leptos_use::docs::{demo_or_body, Note}; +use leptos_use::use_display_media; + +#[component] +fn Demo() -> impl IntoView { + let stream = use_display_media(None); + let video_ref = create_node_ref::(); + + create_effect(move |_| match stream.get() { + Some(Ok(s)) => { + video_ref.get().expect("video element ref not created").set_src_object(Some(&s)); + video_ref.get().map(|v| v.play()); + } + Some(Err(e)) => log::error!("Failed to get media stream: {:?}", e), + None => log::debug!("No stream yet"), + }); + + 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/src/lib.rs b/src/lib.rs index caf6dd5..d42079d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ 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; @@ -76,6 +77,7 @@ 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::*; diff --git a/src/use_display_media.rs b/src/use_display_media.rs new file mode 100644 index 0000000..c68dfc6 --- /dev/null +++ b/src/use_display_media.rs @@ -0,0 +1,29 @@ +use leptos::*; +use wasm_bindgen::{JsValue, JsCast}; +use web_sys::{DisplayMediaStreamConstraints, MediaStream}; +use crate::use_window::use_window; + +async fn create_media(opts: Option) -> Result { + let media = use_window() + .navigator() + .ok_or_else(|| JsValue::from_str("Failed to access window.navigator")) + .and_then(|n| n.media_devices())?; + + let promise = match opts { + Some(o) => media.get_display_media_with_constraints(&o), + None => media.get_display_media(), + }?; + let res = wasm_bindgen_futures::JsFuture::from(promise).await?; + Ok::<_, JsValue>(MediaStream::unchecked_from_js(res)) +} + +type UseDisplayReturn = Resource, Result>; + +pub fn use_display_media(options: S) -> UseDisplayReturn +where + S: Into>>, +{ + let opts: MaybeSignal> = options.into(); + create_local_resource(move || opts.with(|o| o.as_ref().cloned()), create_media) +} + From f34e62fb6b4def61bfad40a58247e80b9f93a9d3 Mon Sep 17 00:00:00 2001 From: Sean Aye Date: Fri, 24 Nov 2023 15:23:30 -0500 Subject: [PATCH 79/90] fix the example via template --- CHANGELOG.md | 4 + docs/book/src/SUMMARY.md | 1 + docs/book/src/browser/use_display_media.md | 2 +- examples/Cargo.toml | 1 + examples/use_display_media/README.md | 23 ++ examples/use_display_media/index.html | 6 +- examples/use_display_media/input.css | 3 + examples/use_display_media/style/output.css | 289 ++++++++++++++++++ examples/use_display_media/tailwind.config.js | 15 + src/lib.rs | 1 + src/use_display_media.rs | 19 ++ 11 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 examples/use_display_media/README.md create mode 100644 examples/use_display_media/input.css create mode 100644 examples/use_display_media/style/output.css create mode 100644 examples/use_display_media/tailwind.config.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e8a12..199ab63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### New Functions 🚀 + +- `use_display_media` + ### Breaking Changes 🛠 - (@feral-dot-io) The use `use__storage` functions have been rewritten to use `Codec`s instead of always diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 35658f4..2cf9fc6 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -33,6 +33,7 @@ - [use_breakpoints](browser/use_breakpoints.md) - [use_color_mode](browser/use_color_mode.md) - [use_css_var](browser/use_css_var.md) +- [use_display_media](browser/use_display_media.md) - [use_event_listener](browser/use_event_listener.md) - [use_favicon](browser/use_favicon.md) - [use_media_query](browser/use_media_query.md) diff --git a/docs/book/src/browser/use_display_media.md b/docs/book/src/browser/use_display_media.md index 5d4219f..6a2d3ed 100644 --- a/docs/book/src/browser/use_display_media.md +++ b/docs/book/src/browser/use_display_media.md @@ -1,3 +1,3 @@ # use_display_media - + diff --git a/examples/Cargo.toml b/examples/Cargo.toml index d9f03b9..462659c 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -14,6 +14,7 @@ members = [ "use_cycle_list", "use_debounce_fn", "use_display_media", + "use_display_media", "use_document_visibility", "use_draggable", "use_drop_zone", diff --git a/examples/use_display_media/README.md b/examples/use_display_media/README.md new file mode 100644 index 0000000..3cc0e27 --- /dev/null +++ b/examples/use_display_media/README.md @@ -0,0 +1,23 @@ +A simple example for `use_display_media`. + +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_display_media/index.html b/examples/use_display_media/index.html index 25f83eb..ae249a6 100644 --- a/examples/use_display_media/index.html +++ b/examples/use_display_media/index.html @@ -1,5 +1,7 @@ - - + + + + diff --git a/examples/use_display_media/input.css b/examples/use_display_media/input.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/examples/use_display_media/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/examples/use_display_media/style/output.css b/examples/use_display_media/style/output.css new file mode 100644 index 0000000..ab5191f --- /dev/null +++ b/examples/use_display_media/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_display_media/tailwind.config.js b/examples/use_display_media/tailwind.config.js new file mode 100644 index 0000000..bc09f5e --- /dev/null +++ b/examples/use_display_media/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/src/lib.rs b/src/lib.rs index d42079d..09811d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,6 +69,7 @@ pub use is_none::*; pub use is_ok::*; pub use is_some::*; pub use on_click_outside::*; +pub use use_display_media::*; pub use signal_debounced::*; pub use signal_throttled::*; pub use use_active_element::*; diff --git a/src/use_display_media.rs b/src/use_display_media.rs index c68dfc6..4d46744 100644 --- a/src/use_display_media.rs +++ b/src/use_display_media.rs @@ -19,6 +19,25 @@ async fn create_media(opts: Option) -> Result, Result>; +/// +/// +/// ## 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; +/// # +/// # #[component] +/// # fn Demo() -> impl IntoView { +/// use_display_media(); +/// # +/// # view! { } +/// # } +/// ``` pub fn use_display_media(options: S) -> UseDisplayReturn where S: Into>>, From ba6082a32f45f89078aba21e0c89fa3e9d543d18 Mon Sep 17 00:00:00 2001 From: Sean Aye Date: Wed, 29 Nov 2023 18:52:39 -0500 Subject: [PATCH 80/90] implement changes --- examples/Cargo.toml | 1 - src/use_display_media.rs | 36 ++++++++++++++++++++---------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 462659c..d9f03b9 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -14,7 +14,6 @@ members = [ "use_cycle_list", "use_debounce_fn", "use_display_media", - "use_display_media", "use_document_visibility", "use_draggable", "use_drop_zone", diff --git a/src/use_display_media.rs b/src/use_display_media.rs index 4d46744..848d2f7 100644 --- a/src/use_display_media.rs +++ b/src/use_display_media.rs @@ -3,23 +3,8 @@ use wasm_bindgen::{JsValue, JsCast}; use web_sys::{DisplayMediaStreamConstraints, MediaStream}; use crate::use_window::use_window; -async fn create_media(opts: Option) -> Result { - let media = use_window() - .navigator() - .ok_or_else(|| JsValue::from_str("Failed to access window.navigator")) - .and_then(|n| n.media_devices())?; - let promise = match opts { - Some(o) => media.get_display_media_with_constraints(&o), - None => media.get_display_media(), - }?; - let res = wasm_bindgen_futures::JsFuture::from(promise).await?; - Ok::<_, JsValue>(MediaStream::unchecked_from_js(res)) -} - -type UseDisplayReturn = Resource, Result>; - -/// +/// Get a media stream from the user's display. /// /// ## Demo /// @@ -46,3 +31,22 @@ where create_local_resource(move || opts.with(|o| o.as_ref().cloned()), create_media) } +async fn create_media(opts: Option) -> Result { + let media = use_window() + .navigator() + .ok_or_else(|| JsValue::from_str("Failed to access window.navigator")) + .and_then(|n| n.media_devices())?; + + let promise = match opts { + Some(o) => media.get_display_media_with_constraints(&o), + None => media.get_display_media(), + }?; + let res = wasm_bindgen_futures::JsFuture::from(promise).await?; + Ok::<_, JsValue>(MediaStream::unchecked_from_js(res)) +} + + +/// A leptos resource which optionally accepts a [DisplayMediaParamContraints](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.DisplayMediaStreamConstraints.html) +/// The resource contains a result containing the media stream or the rejected JsValue +type UseDisplayReturn = Resource, Result>; + From 6c5d9e6f57db0d09c767bcbf542aeee23d7951bc Mon Sep 17 00:00:00 2001 From: Sean Aye Date: Wed, 29 Nov 2023 18:55:22 -0500 Subject: [PATCH 81/90] update doc comment --- src/use_display_media.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/use_display_media.rs b/src/use_display_media.rs index 848d2f7..9a645bf 100644 --- a/src/use_display_media.rs +++ b/src/use_display_media.rs @@ -4,7 +4,7 @@ use web_sys::{DisplayMediaStreamConstraints, MediaStream}; use crate::use_window::use_window; -/// Get a media stream from the user's display. +/// Get a Resource containing a media stream from the user's display. /// /// ## Demo /// From 50c3e14f77a8187f811f8166a07698351f1b7b65 Mon Sep 17 00:00:00 2001 From: Sean Aye Date: Thu, 30 Nov 2023 08:01:08 -0500 Subject: [PATCH 82/90] update example --- src/use_display_media.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/use_display_media.rs b/src/use_display_media.rs index 9a645bf..15d54f9 100644 --- a/src/use_display_media.rs +++ b/src/use_display_media.rs @@ -18,9 +18,19 @@ use crate::use_window::use_window; /// # /// # #[component] /// # fn Demo() -> impl IntoView { -/// use_display_media(); +/// # let stream = use_display_media(None); /// # -/// # view! { } +/// # let video_ref = create_node_ref::(); +/// # create_effect(move |_| match stream.get() { +/// # Some(Ok(s)) => { +/// # video_ref.get().expect("video element ref not created").set_src_object(Some(&s)); +/// # video_ref.get().map(|v| v.play()); +/// # } +/// # Some(Err(e)) => log::error!("Failed to get media stream: {:?}", e), +/// # None => log::debug!("No stream yet"), +/// # }); +/// # +/// # view! { } /// # } /// ``` pub fn use_display_media(options: S) -> UseDisplayReturn From bb5054d403cb78aab02fb0430f75edc022108842 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Tue, 5 Dec 2023 23:12:31 +0000 Subject: [PATCH 83/90] made display media api closer to solidjs-use and rest of library --- CHANGELOG.md | 2 +- Cargo.toml | 1 + examples/use_display_media/src/main.rs | 49 +++-- examples/use_display_media/style/output.css | 63 ++++++- src/core/maybe_rw_signal.rs | 11 ++ src/lib.rs | 1 - src/use_display_media.rs | 193 ++++++++++++++++---- 7 files changed, 269 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 199ab63..eaea869 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New Functions 🚀 -- `use_display_media` +- `use_display_media` (thanks to @seanaye) ### Breaking Changes 🛠 diff --git a/Cargo.toml b/Cargo.toml index a4bcbda..d5d324b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ features = [ "MediaDevices", "MediaQueryList", "MediaStream", + "MediaStreamTrack", "MouseEvent", "MutationObserver", "MutationObserverInit", diff --git a/examples/use_display_media/src/main.rs b/examples/use_display_media/src/main.rs index 01cb48e..d1d50d8 100644 --- a/examples/use_display_media/src/main.rs +++ b/examples/use_display_media/src/main.rs @@ -1,22 +1,50 @@ use leptos::*; -use leptos_use::docs::{demo_or_body, Note}; -use leptos_use::use_display_media; +use leptos_use::docs::demo_or_body; +use leptos_use::{use_display_media, UseDisplayMediaReturn}; #[component] fn Demo() -> impl IntoView { - let stream = use_display_media(None); let video_ref = create_node_ref::(); - create_effect(move |_| match stream.get() { - Some(Ok(s)) => { - video_ref.get().expect("video element ref not created").set_src_object(Some(&s)); - video_ref.get().map(|v| v.play()); + let UseDisplayMediaReturn { + stream, + enabled, + set_enabled, + .. + } = use_display_media(); + + create_effect(move |_| { + match stream.get() { + Some(Ok(s)) => { + video_ref.get().map(|v| v.set_src_object(Some(&s))); + return; + } + Some(Err(e)) => logging::error!("Failed to get media stream: {:?}", e), + None => logging::log!("No stream yet"), } - Some(Err(e)) => log::error!("Failed to get media stream: {:?}", e), - None => log::debug!("No stream yet"), + + video_ref.get().map(|v| v.set_src_object(None)); }); - view! { } + view! { +
+
+ +
+ +
+ +
+
+ } } fn main() { @@ -27,4 +55,3 @@ fn main() { view! { } }) } - diff --git a/examples/use_display_media/style/output.css b/examples/use_display_media/style/output.css index ab5191f..291c6c5 100644 --- a/examples/use_display_media/style/output.css +++ b/examples/use_display_media/style/output.css @@ -1,4 +1,4 @@ -[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 { +[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; @@ -15,7 +15,7 @@ --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 { +[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,/*!*/ /*!*/); @@ -44,6 +44,11 @@ input::placeholder,textarea::placeholder { ::-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 { @@ -61,7 +66,7 @@ select { print-color-adjust: exact; } -[multiple] { +[multiple],[size]:where(select:not([size="1"])) { background-image: initial; background-position: initial; background-repeat: unset; @@ -126,10 +131,26 @@ select { 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"); } +@media (forced-colors: active) { + [type='checkbox']:checked { + -webkit-appearance: auto; + -moz-appearance: auto; + appearance: auto; + } +} + [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"); } +@media (forced-colors: active) { + [type='radio']:checked { + -webkit-appearance: auto; + -moz-appearance: auto; + appearance: auto; + } +} + [type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus { border-color: transparent; background-color: currentColor; @@ -144,6 +165,14 @@ select { background-repeat: no-repeat; } +@media (forced-colors: active) { + [type='checkbox']:indeterminate { + -webkit-appearance: auto; + -moz-appearance: auto; + appearance: auto; + } +} + [type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { border-color: transparent; background-color: currentColor; @@ -264,8 +293,32 @@ select { --tw-backdrop-sepia: ; } -.block { - display: block; +.static { + position: static; +} + +.flex { + display: flex; +} + +.h-96 { + height: 24rem; +} + +.w-auto { + width: auto; +} + +.flex-col { + flex-direction: column; +} + +.gap-4 { + gap: 1rem; +} + +.text-center { + text-align: center; } .text-\[--brand-color\] { 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/lib.rs b/src/lib.rs index 09811d6..d42079d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,7 +69,6 @@ pub use is_none::*; pub use is_ok::*; pub use is_some::*; pub use on_click_outside::*; -pub use use_display_media::*; pub use signal_debounced::*; pub use signal_throttled::*; pub use use_active_element::*; diff --git a/src/use_display_media.rs b/src/use_display_media.rs index 15d54f9..2dbc5f4 100644 --- a/src/use_display_media.rs +++ b/src/use_display_media.rs @@ -1,10 +1,12 @@ -use leptos::*; -use wasm_bindgen::{JsValue, JsCast}; -use web_sys::{DisplayMediaStreamConstraints, MediaStream}; +use crate::core::MaybeRwSignal; use crate::use_window::use_window; +use cfg_if::cfg_if; +use default_struct_builder::DefaultBuilder; +use leptos::*; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{DisplayMediaStreamConstraints, MediaStream}; - -/// Get a Resource containing a media stream from the user's display. +/// Reactive [`mediaDevices.getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) streaming. /// /// ## Demo /// @@ -14,49 +16,174 @@ use crate::use_window::use_window; /// /// ``` /// # use leptos::*; -/// # use leptos_use::use_display_media; +/// # use leptos_use::{use_display_media, UseDisplayMediaReturn}; /// # /// # #[component] /// # fn Demo() -> impl IntoView { -/// # let stream = use_display_media(None); -/// # -/// # let video_ref = create_node_ref::(); -/// # create_effect(move |_| match stream.get() { -/// # Some(Ok(s)) => { -/// # video_ref.get().expect("video element ref not created").set_src_object(Some(&s)); -/// # video_ref.get().map(|v| v.play()); -/// # } -/// # Some(Err(e)) => log::error!("Failed to get media stream: {:?}", e), -/// # None => log::debug!("No stream yet"), -/// # }); -/// # -/// # view! { } +/// 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! { } /// # } /// ``` -pub fn use_display_media(options: S) -> UseDisplayReturn -where - S: Into>>, -{ - let opts: MaybeSignal> = options.into(); - create_local_resource(move || opts.with(|o| o.as_ref().cloned()), create_media) +/// +/// ## 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()) } -async fn create_media(opts: Option) -> Result { +/// 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)); + }} + }; + + 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 { let media = use_window() .navigator() .ok_or_else(|| JsValue::from_str("Failed to access window.navigator")) .and_then(|n| n.media_devices())?; - let promise = match opts { - Some(o) => media.get_display_media_with_constraints(&o), - None => media.get_display_media(), - }?; + let mut constraints = 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>(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 leptos resource which optionally accepts a [DisplayMediaParamContraints](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.DisplayMediaStreamConstraints.html) -/// The resource contains a result containing the media stream or the rejected JsValue -type UseDisplayReturn = Resource, Result>; + /// 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, +} From 68116b87919f56f7b2dfcf7f5b7bb20cd3f142ac Mon Sep 17 00:00:00 2001 From: Maccesch Date: Tue, 5 Dec 2023 23:32:51 +0000 Subject: [PATCH 84/90] fixes #57 (use_idle) --- src/use_idle.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/use_idle.rs b/src/use_idle.rs index 1819929..3f0a20e 100644 --- a/src/use_idle.rs +++ b/src/use_idle.rs @@ -90,24 +90,28 @@ pub fn use_idle_with_options( let _ = set_idle; } else { use crate::utils::create_filter_wrapper; - use crate::{use_document, use_event_listener, use_event_listener_with_options,UseEventListenerOptions}; + 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; + let timer = Rc::new(Cell::new(None::)); + let reset = { - let timer = Cell::new(None::); + let timer = Rc::clone(&timer); move || { set_idle.set(false); - if let Some(timer) = timer.take() { - timer.clear(); - } - timer.replace( + if let Some(timer) = timer.replace( set_timeout_with_handle(move || set_idle.set(true), Duration::from_millis(timeout)) .ok(), - ); + ) { + timer.clear(); + } } }; From a3dc18064f9b6a4cc86eb7c6c4c95bd4bf7ee0af Mon Sep 17 00:00:00 2001 From: Maccesch Date: Tue, 5 Dec 2023 23:35:07 +0000 Subject: [PATCH 85/90] udpated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaea869..30bc2db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `use_infinite_scroll` no longer calls the callback twice for the same event - `use_scroll` now uses `try_get_untracked` in the debounced callback to avoid panics if the context has been destroyed while the callback was waiting to be called. +- `use_idle` works properly now (no more idles too early). ## [0.8.2] - 2023-11-09 From 5f5779ed100a552c1b4571cc68b56a502265a673 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 6 Dec 2023 00:10:33 +0000 Subject: [PATCH 86/90] use_web_notification is now SSR-safe. relates to #46 --- CHANGELOG.md | 1 + src/use_display_media.rs | 16 +-- src/use_service_worker.rs | 4 + src/use_web_notification.rs | 192 +++++++++++++++++++----------------- src/use_websocket.rs | 2 +- 5 files changed, 119 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30bc2db..97a94ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `use_scroll` now uses `try_get_untracked` in the debounced callback to avoid panics if the context has been destroyed while the callback was waiting to be called. - `use_idle` works properly now (no more idles too early). +- `use_web_notification` doesn't panic on the server anymore. ## [0.8.2] - 2023-11-09 diff --git a/src/use_display_media.rs b/src/use_display_media.rs index 2dbc5f4..fb0ff8c 100644 --- a/src/use_display_media.rs +++ b/src/use_display_media.rs @@ -1,10 +1,8 @@ use crate::core::MaybeRwSignal; -use crate::use_window::use_window; use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::*; use wasm_bindgen::{JsCast, JsValue}; -use web_sys::{DisplayMediaStreamConstraints, MediaStream}; /// Reactive [`mediaDevices.getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) streaming. /// @@ -56,7 +54,7 @@ pub fn use_display_media_with_options( let (enabled, set_enabled) = enabled.into_signal(); - let (stream, set_stream) = create_signal(None::>); + let (stream, set_stream) = create_signal(None::>); let _start = move || async move { cfg_if! { if #[cfg(not(feature = "ssr"))] { @@ -67,6 +65,8 @@ pub fn use_display_media_with_options( let stream = create_media(audio).await; set_stream.update(|s| *s = Some(stream)); + } else { + let _ = audio; }} }; @@ -122,13 +122,15 @@ pub fn use_display_media_with_options( } #[cfg(not(feature = "ssr"))] -async fn create_media(audio: bool) -> Result { +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 = DisplayMediaStreamConstraints::new(); + let mut constraints = web_sys::DisplayMediaStreamConstraints::new(); if audio { constraints.audio(&JsValue::from(true)); } @@ -136,7 +138,7 @@ async fn create_media(audio: bool) -> Result { let promise = media.get_display_media_with_constraints(&constraints)?; let res = wasm_bindgen_futures::JsFuture::from(promise).await?; - Ok::<_, JsValue>(MediaStream::unchecked_from_js(res)) + 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. @@ -172,7 +174,7 @@ where /// 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>>, + pub stream: Signal>>, /// Starts the screen streaming. Triggers the ask for permission if not already granted. pub start: StartFn, diff --git a/src/use_service_worker.rs b/src/use_service_worker.rs index ec2d262..b9dda2d 100644 --- a/src/use_service_worker.rs +++ b/src/use_service_worker.rs @@ -33,6 +33,10 @@ use crate::use_window; /// # view! { } /// # } /// ``` +/// +/// ## Server-Side Rendering +/// +/// This function does **not** support SSR. Call it inside a `create_effect`. pub fn use_service_worker() -> UseServiceWorkerReturn { use_service_worker_with_options(UseServiceWorkerOptions::default()) } diff --git a/src/use_web_notification.rs b/src/use_web_notification.rs index 98c66ef..1b8ca89 100644 --- a/src/use_web_notification.rs +++ b/src/use_web_notification.rs @@ -1,10 +1,8 @@ -use crate::{use_event_listener, use_supported, use_window}; +use crate::{use_supported, use_window}; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; -use leptos::ev::visibilitychange; use leptos::*; use std::rc::Rc; -use wasm_bindgen::closure::Closure; -use wasm_bindgen::JsCast; /// Reactive [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/Notification). /// @@ -53,108 +51,122 @@ pub fn use_web_notification_with_options( let (permission, set_permission) = create_signal(NotificationPermission::default()); - let on_click_closure = Closure::::new({ - let on_click = Rc::clone(&options.on_click); - move |e: web_sys::Event| { - on_click(e); - } - }) - .into_js_value(); + cfg_if! { if #[cfg(feature = "ssr")] { + let _ = options; + let _ = set_notification; + let _ = set_permission; - let on_close_closure = Closure::::new({ - let on_close = Rc::clone(&options.on_close); - move |e: web_sys::Event| { - on_close(e); - } - }) - .into_js_value(); + let show = move |_: ShowOptions| (); + let close = move || (); + } else { + use crate::use_event_listener; + use leptos::ev::visibilitychange; + use wasm_bindgen::closure::Closure; + use wasm_bindgen::JsCast; - let on_error_closure = Closure::::new({ - let on_error = Rc::clone(&options.on_error); - move |e: web_sys::Event| { - on_error(e); - } - }) - .into_js_value(); - - let on_show_closure = Closure::::new({ - let on_show = Rc::clone(&options.on_show); - move |e: web_sys::Event| { - on_show(e); - } - }) - .into_js_value(); - - let show = { - let options = options.clone(); - let on_click_closure = on_click_closure.clone(); - let on_close_closure = on_close_closure.clone(); - let on_error_closure = on_error_closure.clone(); - let on_show_closure = on_show_closure.clone(); - - move |options_override: ShowOptions| { - if !is_supported.get_untracked() { - return; + let on_click_closure = Closure::::new({ + let on_click = Rc::clone(&options.on_click); + move |e: web_sys::Event| { + on_click(e); } + }) + .into_js_value(); + let on_close_closure = Closure::::new({ + let on_close = Rc::clone(&options.on_close); + move |e: web_sys::Event| { + on_close(e); + } + }) + .into_js_value(); + + let on_error_closure = Closure::::new({ + let on_error = Rc::clone(&options.on_error); + move |e: web_sys::Event| { + on_error(e); + } + }) + .into_js_value(); + + let on_show_closure = Closure::::new({ + let on_show = Rc::clone(&options.on_show); + move |e: web_sys::Event| { + on_show(e); + } + }) + .into_js_value(); + + let show = { let options = options.clone(); let on_click_closure = on_click_closure.clone(); let on_close_closure = on_close_closure.clone(); let on_error_closure = on_error_closure.clone(); let on_show_closure = on_show_closure.clone(); - spawn_local(async move { - set_permission.set(request_web_notification_permission().await); + move |options_override: ShowOptions| { + if !is_supported.get_untracked() { + return; + } - let mut notification_options = web_sys::NotificationOptions::from(&options); - options_override.override_notification_options(&mut notification_options); + let options = options.clone(); + let on_click_closure = on_click_closure.clone(); + let on_close_closure = on_close_closure.clone(); + let on_error_closure = on_error_closure.clone(); + let on_show_closure = on_show_closure.clone(); - let notification_value = web_sys::Notification::new_with_options( - &options_override.title.unwrap_or(options.title), - ¬ification_options, - ) - .expect("Notification should be created"); + spawn_local(async move { + set_permission.set(request_web_notification_permission().await); - notification_value.set_onclick(Some(on_click_closure.unchecked_ref())); - notification_value.set_onclose(Some(on_close_closure.unchecked_ref())); - notification_value.set_onerror(Some(on_error_closure.unchecked_ref())); - notification_value.set_onshow(Some(on_show_closure.unchecked_ref())); + let mut notification_options = web_sys::NotificationOptions::from(&options); + options_override.override_notification_options(&mut notification_options); - set_notification.set(Some(notification_value)); - }); - } - }; + let notification_value = web_sys::Notification::new_with_options( + &options_override.title.unwrap_or(options.title), + ¬ification_options, + ) + .expect("Notification should be created"); - let close = { - move || { - notification.with_untracked(|notification| { - if let Some(notification) = notification { - notification.close(); + notification_value.set_onclick(Some(on_click_closure.unchecked_ref())); + notification_value.set_onclose(Some(on_close_closure.unchecked_ref())); + notification_value.set_onerror(Some(on_error_closure.unchecked_ref())); + notification_value.set_onshow(Some(on_show_closure.unchecked_ref())); + + set_notification.set(Some(notification_value)); + }); + } + }; + + let close = { + move || { + notification.with_untracked(|notification| { + if let Some(notification) = notification { + notification.close(); + } + }); + set_notification.set(None); + } + }; + + spawn_local(async move { + set_permission.set(request_web_notification_permission().await); + }); + + on_cleanup(close); + + // Use close() to remove a notification that is no longer relevant to to + // the user (e.g.the user already read the notification on the webpage). + // Most modern browsers dismiss notifications automatically after a few + // moments(around four seconds). + if is_supported.get_untracked() { + let _ = use_event_listener(document(), visibilitychange, move |e: web_sys::Event| { + e.prevent_default(); + if document().visibility_state() == web_sys::VisibilityState::Visible { + // The tab has become visible so clear the now-stale Notification: + close() } }); - set_notification.set(None); } - }; - - spawn_local(async move { - set_permission.set(request_web_notification_permission().await); - }); - - on_cleanup(close); - - // Use close() to remove a notification that is no longer relevant to to - // the user (e.g.the user already read the notification on the webpage). - // Most modern browsers dismiss notifications automatically after a few - // moments(around four seconds). - if is_supported.get_untracked() { - let _ = use_event_listener(document(), visibilitychange, move |e: web_sys::Event| { - e.prevent_default(); - if document().visibility_state() == web_sys::VisibilityState::Visible { - // The tab has become visible so clear the now-stale Notification: - close() - } - }); - } + }} UseWebNotificationReturn { is_supported, @@ -192,6 +204,7 @@ impl From for web_sys::NotificationDirection { /// - `silent` /// - `image` #[derive(DefaultBuilder, Clone)] +#[cfg_attr(feature = "ssr", allow(dead_code))] pub struct UseWebNotificationOptions { /// The title property of the Notification interface indicates /// the title of the notification @@ -302,6 +315,7 @@ impl From<&UseWebNotificationOptions> for web_sys::NotificationOptions { /// - `silent` /// - `image` #[derive(DefaultBuilder, Default)] +#[cfg_attr(feature = "ssr", allow(dead_code))] pub struct ShowOptions { /// The title property of the Notification interface indicates /// the title of the notification @@ -344,6 +358,7 @@ pub struct ShowOptions { // renotify: Option, } +#[cfg(not(feature = "ssr"))] impl ShowOptions { fn override_notification_options(&self, options: &mut web_sys::NotificationOptions) { if let Some(direction) = self.direction { @@ -413,6 +428,7 @@ impl From for NotificationPermission { /// Use `window.Notification.requestPosition()`. Returns a future that should be awaited /// at least once before using [`use_web_notification`] to make sure /// you have the permission to send notifications. +#[cfg(not(feature = "ssr"))] async fn request_web_notification_permission() -> NotificationPermission { if let Ok(notification_permission) = web_sys::Notification::request_permission() { let _ = wasm_bindgen_futures::JsFuture::from(notification_permission).await; diff --git a/src/use_websocket.rs b/src/use_websocket.rs index 5b81a9e..9f40d4f 100644 --- a/src/use_websocket.rs +++ b/src/use_websocket.rs @@ -485,7 +485,7 @@ pub struct UseWebSocketOptions { /// If `false` you have to manually call the `open` function. /// Defaults to `true`. immediate: bool, - /// Sub protocols + /// Sub protocols. See [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols). protocols: Option>, } From 67a2bee7c3ca48385b5e603304abd48e2558c30c Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 6 Dec 2023 00:23:58 +0000 Subject: [PATCH 87/90] release 0.9.0 --- CHANGELOG.md | 2 +- Cargo.toml | 2 +- README.md | 4 ++-- docs/book/src/introduction.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97a94ca..66c789d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.9.0] - 2023-12-06 ### New Functions 🚀 diff --git a/Cargo.toml b/Cargo.toml index d5d324b..f770daf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos-use" -version = "0.8.2" +version = "0.9.0" edition = "2021" authors = ["Marc-Stefan Cassola"] categories = ["gui", "web-programming"] diff --git a/README.md b/README.md index e2771df..7fa64c7 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Crates.io SSR Docs & Demos - 62 Functions + 63 Functions


@@ -91,4 +91,4 @@ This will create the function file in the src directory, scaffold an example dir |----------------|---------------------------| | <= 0.3 | 0.3 | | 0.4, 0.5, 0.6 | 0.4 | -| 0.7, 0.8 | 0.5 | +| 0.7, 0.8, 0.9 | 0.5 | diff --git a/docs/book/src/introduction.md b/docs/book/src/introduction.md index d9a1704..a8c07ae 100644 --- a/docs/book/src/introduction.md +++ b/docs/book/src/introduction.md @@ -12,6 +12,6 @@ Crates.io SSR Docs & Demos - 62 Functions + 63 Functions

\ No newline at end of file From 72b6f313781962275bdeff452abe6abd57de8fd0 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 6 Dec 2023 00:39:53 +0000 Subject: [PATCH 88/90] added ssr docs to use_web_notification --- src/use_web_notification.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/use_web_notification.rs b/src/use_web_notification.rs index 1b8ca89..44b52a6 100644 --- a/src/use_web_notification.rs +++ b/src/use_web_notification.rs @@ -36,6 +36,10 @@ use std::rc::Rc; /// # view! { } /// # } /// ``` +/// +/// ## Server-Side Rendering +/// +/// This function is basically ignored on the server. You can safely call `show` but it will do nothing. pub fn use_web_notification( ) -> UseWebNotificationReturn { use_web_notification_with_options(UseWebNotificationOptions::default()) From cf864712191a265710080fe2bac4e6990b3064cb Mon Sep 17 00:00:00 2001 From: Maccesch Date: Sat, 9 Dec 2023 15:20:18 +0000 Subject: [PATCH 89/90] small docs improvement for use_color_mode --- src/use_color_mode.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index 5f2b781..d2dcec4 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -21,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 { @@ -44,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 { From bc87a3015d6f5328985514b44c152e8bbb34d0b8 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Mon, 11 Dec 2023 19:13:16 +0000 Subject: [PATCH 90/90] small docs improvement for use_color_mode --- src/use_color_mode.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/use_color_mode.rs b/src/use_color_mode.rs index d2dcec4..52e829d 100644 --- a/src/use_color_mode.rs +++ b/src/use_color_mode.rs @@ -64,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 {