diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e5d89ef --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,70 @@ +on: + push: + # Pattern matched against refs/tags + tags: + - '*' # Push events to every tag not containing / + workflow_dispatch: + +name: CI + +jobs: + publish: + name: Publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + profile: minimal + override: true + components: rustfmt + - name: Cache + uses: Swatinem/rust-cache@v2 + - name: Check formatting + run: cargo fmt --check +# - name: Check if the README is up to date. +# run: | +# cargo install cargo-rdme +# cargo rdme --check + - name: Run tests + run: cargo test --all-features + - name: Publish crate leptos-use + uses: katyo/publish-crates@v2 + with: + registry-token: ${{ secrets.CRATES_TOKEN }} + +# coverage: +# name: Coverage +# runs-on: ubuntu-latest +# +# steps: +# - name: Checkout sources +# uses: actions/checkout@v2 +# +# - name: Install rust +# uses: actions-rs/toolchain@v1 +# with: +# toolchain: stable +# profile: minimal +# override: true +# +# - name: Cache +# uses: Swatinem/rust-cache@v1 +# +# - name: Install cargo-tarpaulin +# uses: actions-rs/cargo@v1 +# with: +# command: install +# args: cargo-tarpaulin +# +# - name: Run cargo tarpaulin +# uses: actions-rs/cargo@v1 +# with: +# command: tarpaulin +# args: --output-dir coverage --out Lcov +# +# - name: Publish to Coveralls +# uses: coverallsapp/github-action@master +# with: +# github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.idea/leptos-use.iml b/.idea/leptos-use.iml index c254557..db5be54 100644 --- a/.idea/leptos-use.iml +++ b/.idea/leptos-use.iml @@ -2,7 +2,10 @@ + + + diff --git a/Cargo.toml b/Cargo.toml index e981714..f41ea7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "leptos-use" -version = "0.1.0" +version = "0.1.1" edition = "2021" authors = ["Marc-Stefan Cassola"] categories = ["gui", "web-programming"] @@ -8,9 +8,11 @@ description = "Collection of essential Leptos utilities inspired by SolidJS USE exclude = ["examples/", "tests/"] keywords = ["leptos", "utilities"] license = "MIT OR Apache-2.0" -# readme = "README.md" +readme = "README.md" repository = "https://github.com/Synphonyte/leptos-use" [dependencies] -leptos = "0.2" \ No newline at end of file +leptos = "0.2" +web-sys = "0.3" +wasm-bindgen = "0.2" diff --git a/README.md b/README.md new file mode 100644 index 0000000..8943695 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# Leptos-Use + +[![Crates.io](https://img.shields.io/crates/v/leptos-use.svg)](https://crates.io/crates/leptos-use) +[![Docs](https://docs.rs/leptos-use/badge.svg)](https://docs.rs/leptos-use/) +[![MIT/Apache 2.0](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/synphonyte/leptos-use#license) +[![Build Status](https://github.com/synphonyte/leptos-use/actions/workflows/ci.yml/badge.svg)](https://github.com/synphonyte/leptos-use/actions/workflows/ci.yml) + +Collection of essential Leptos utilities inspired by SolidJS-USE / VueUse + +This is still very much work in progress and all contributions are welcome! \ No newline at end of file diff --git a/examples/use_event_listener/Cargo.toml b/examples/use_event_listener/Cargo.toml new file mode 100644 index 0000000..26b59e5 --- /dev/null +++ b/examples/use_event_listener/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "use_event_listener" +version = "0.1.0" +edition = "2021" + +[dependencies] +leptos = "0.2" +console_error_panic_hook = "0.1" +console_log = "1" +log = "0.4" +leptos-use = { path = "../.." } +web-sys = "0.3" + +[dev-dependencies] +wasm-bindgen = "0.2" +wasm-bindgen-test = "0.3.0" diff --git a/examples/use_event_listener/README.md b/examples/use_event_listener/README.md new file mode 100644 index 0000000..81e444f --- /dev/null +++ b/examples/use_event_listener/README.md @@ -0,0 +1,16 @@ +A simple example for `use_event_listener`. + +If you don't have it installed already, install [Trunk](https://trunkrs.dev/) +as well as the nightly toolchain for Rust and the wasm32-unknown-unknown target: + +```bash +cargo install trunk +rustup toolchain install nightly +rustup target add wasm32-unknown-unknown +``` + +Then, to run this example, execute in a terminal: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/use_event_listener/index.html b/examples/use_event_listener/index.html new file mode 100644 index 0000000..a4acdc4 --- /dev/null +++ b/examples/use_event_listener/index.html @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/use_event_listener/rust-toolchain.toml b/examples/use_event_listener/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/examples/use_event_listener/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/examples/use_event_listener/src/main.rs b/examples/use_event_listener/src/main.rs new file mode 100644 index 0000000..4ef345f --- /dev/null +++ b/examples/use_event_listener/src/main.rs @@ -0,0 +1,48 @@ +use leptos::ev::click; +use leptos::*; +use leptos_use::use_event_listener_ref; +use web_sys::HtmlDivElement; + +#[component] +fn Demo(cx: Scope) -> impl IntoView { + let element = create_node_ref(cx); + + let _ = use_event_listener_ref(cx, element, click, |evt| { + log!( + "click from element {:?}", + event_target::(&evt) + ); + }); + + let (cond, set_cond) = create_signal(cx, true); + + view! { cx, +

"Check in the dev tools console"

+

+ +

+ "Condition false [click me]" } + > +
"Condition true [click me]"
+
+ } +} + +fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + mount_to_body(|cx| { + view! {cx, + + } + }) +} diff --git a/src/lib.rs b/src/lib.rs index 7d12d9a..700bd93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,3 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} +mod use_event_listener; -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub use use_event_listener::*; diff --git a/src/use_event_listener.rs b/src/use_event_listener.rs new file mode 100644 index 0000000..0961af9 --- /dev/null +++ b/src/use_event_listener.rs @@ -0,0 +1,247 @@ +use leptos::ev::EventDescriptor; +use leptos::html::ElementDescriptor; +use leptos::*; +use std::cell::RefCell; +use std::ops::Deref; +use std::rc::Rc; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::JsCast; + +/// Use EventListener with ease. Register using [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) on mounted, +/// and [removeEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) automatically on cleanup. +/// +/// ## Usage +/// +/// ``` +/// use leptos::*; +/// use leptos::ev::visibilitychange; +/// use leptos_use::use_event_listener; +/// +/// #[component] +/// fn Demo(cx: Scope) -> impl IntoView { +/// use_event_listener(cx, Some(document()), visibilitychange, |evt| { +/// log!("{:?}", evt); +/// }); +/// +/// view! { cx, } +/// } +/// ``` +/// +/// You can also pass a [NodeRef](leptos::NodeRef) as the event target, `use_event_listener` will unregister the previous event and register +/// the new one when you change the target. (For now you have to use `use_event_listener_ref` to do that) +/// +/// ``` +/// use leptos::*; +/// use leptos::ev::click; +/// use leptos_use::use_event_listener_ref; +/// +/// #[component] +/// fn Demo(cx: Scope) -> impl IntoView { +/// let element = create_node_ref(cx); +/// +/// use_event_listener_ref(cx, element, click, |evt| { +/// log!("click from element {:?}", event_target::(&evt)); +/// }); +/// +/// let (cond, set_cond) = create_signal(cx, true); +/// +/// view! { cx, +/// "Condition false" } +/// > +///
"Condition true"
+///
+/// } +/// } +/// ``` +/// +/// You can also call the returned to unregister the listener. +/// +/// ``` +/// # use leptos::*; +/// # use leptos::ev::keydown; +/// # use web_sys::KeyboardEvent; +/// # use leptos_use::use_event_listener; +/// # +/// # #[component] +/// # fn Demo(cx: Scope) -> impl IntoView { +/// let cleanup = use_event_listener(cx, Some(document()), keydown, |evt: KeyboardEvent| { +/// log!("{}", &evt.key()); +/// }); +/// +/// cleanup(); +/// # +/// # view! { cx, } +/// # } +/// ``` +/// +/// Note if your components also run in SSR (Server Side Rendering), you might get errors +/// because DOM APIs like document and window are not available outside of the browser. +/// To avoid that you can put the logic inside a [`create_effect`](leptos::create_effect) hook +/// which only runs client side. +#[allow(unused_must_use)] +pub fn use_event_listener( + cx: Scope, + target: El, + event: Ev, + handler: F, +) -> Box +where + Ev: EventDescriptor + 'static, + El: Into>>, + Inner: Into + Clone + 'static, + F: FnMut(::EventType) + 'static, +{ + let event_name = event.name(); + let closure_js = Closure::wrap(Box::new(handler) as Box).into_js_value(); + + let closure = closure_js.clone(); + let cleanup_fn = move |element: &web_sys::EventTarget| { + let _ = element + .remove_event_listener_with_callback(&event_name, closure.as_ref().unchecked_ref()); + }; + let cleanup = cleanup_fn.clone(); + + let event_name = event.name(); + + match target.into() { + MaybeSignal::Static(element) => { + if let Some(element) = element { + let element = element.into(); + _ = element.add_event_listener_with_callback( + &event_name, + closure_js.as_ref().unchecked_ref(), + ); + + let cleanup_fn = move || { + cleanup(&element); + }; + on_cleanup(cx, cleanup_fn.clone()); + + Box::new(cleanup_fn) + } else { + Box::new(|| {}) + } + } + MaybeSignal::Dynamic(signal) => { + let element = signal.get_untracked(); + + let cleanup_prev_element = if let Some(element) = element { + let element = element.into(); + + _ = element.add_event_listener_with_callback( + &event_name, + closure_js.as_ref().unchecked_ref(), + ); + + let clean = cleanup.clone(); + Rc::new(RefCell::new(Box::new(move || { + clean(&element); + }) as Box)) + } else { + Rc::new(RefCell::new(Box::new(move || {}) as Box)) + }; + + let cleanup_prev_el = Rc::clone(&cleanup_prev_element); + let closure = closure_js.clone(); + create_effect(cx, move |_| { + cleanup_prev_el.borrow()(); + + let element = signal(); + + if let Some(element) = element { + let element = element.into(); + + _ = element.add_event_listener_with_callback( + &event_name, + closure.as_ref().unchecked_ref(), + ); + + let clean = cleanup.clone(); + cleanup_prev_el.replace(Box::new(move || { + clean(&element); + }) as Box); + } else { + cleanup_prev_el.replace(Box::new(move || {}) as Box); + } + }); + + let cleanup_fn = move || cleanup_prev_element.borrow()(); + on_cleanup(cx, cleanup_fn.clone()); + + Box::new(cleanup_fn) + } + } +} + +/// Version for using with [NodeRef](leptos::NodeRef). See [use_event_listener] for how to use. +#[allow(unused_must_use)] +pub fn use_event_listener_ref( + cx: Scope, + target: NodeRef, + event: Ev, + handler: F, +) -> Box +where + Ev: EventDescriptor + 'static, + El: ElementDescriptor + Clone, + F: FnMut(::EventType) + 'static, +{ + let event_name = event.name(); + let closure_js = Closure::wrap(Box::new(handler) as Box).into_js_value(); + + let closure = closure_js.clone(); + let cleanup_fn = move |element: &web_sys::EventTarget| { + let _ = element + .remove_event_listener_with_callback(&event_name, closure.as_ref().unchecked_ref()); + }; + let cleanup = cleanup_fn.clone(); + + let event_name = event.name(); + + let element = target.get(); + + let cleanup_prev_element = if let Some(element) = element { + let element = element.into_any(); + let element: web_sys::EventTarget = element.deref().clone().into(); + + _ = element + .add_event_listener_with_callback(&event_name, closure_js.as_ref().unchecked_ref()); + + let clean = cleanup.clone(); + Rc::new(RefCell::new(Box::new(move || { + clean(&element); + }) as Box)) + } else { + Rc::new(RefCell::new(Box::new(move || {}) as Box)) + }; + + let cleanup_prev_el = Rc::clone(&cleanup_prev_element); + let closure = closure_js.clone(); + create_effect(cx, move |_| { + cleanup_prev_el.borrow()(); + + let element = target.get(); + + if let Some(element) = element { + let element = element.into_any(); + let element: web_sys::EventTarget = element.deref().clone().into(); + + _ = element + .add_event_listener_with_callback(&event_name, closure.as_ref().unchecked_ref()); + + let clean = cleanup.clone(); + cleanup_prev_el.replace(Box::new(move || { + clean(&element); + }) as Box); + } else { + cleanup_prev_el.replace(Box::new(move || {}) as Box); + } + }); + + let cleanup_fn = move || cleanup_prev_element.borrow()(); + on_cleanup(cx, cleanup_fn.clone()); + + Box::new(cleanup_fn) +}