added use_event_listener

This commit is contained in:
Maccesch 2023-05-13 23:05:08 +01:00
parent a683430dbc
commit b72acc8f65
11 changed files with 424 additions and 16 deletions

70
.github/workflows/ci.yml vendored Normal file
View file

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

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

@ -2,7 +2,10 @@
<module type="CPP_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_event_listener/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/examples/use_event_listener/target" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />

View file

@ -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"
leptos = "0.2"
web-sys = "0.3"
wasm-bindgen = "0.2"

10
README.md Normal file
View file

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

View file

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

View file

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

View file

@ -0,0 +1,5 @@
<!DOCTYPE html>
<html>
<head></head>
<body></body>
</html>

View file

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

View file

@ -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::<HtmlDivElement>(&evt)
);
});
let (cond, set_cond) = create_signal(cx, true);
view! { cx,
<p>"Check in the dev tools console"</p>
<p>
<label>
<input
type="checkbox" on:change=move |evt| set_cond(event_target_checked(&evt))
prop:checked=cond
/>
"Condition enabled"
</label>
</p>
<Show
when=move || cond()
fallback=move |cx| view! { cx, <div node_ref=element>"Condition false [click me]"</div> }
>
<div node_ref=element>"Condition true [click me]"</div>
</Show>
}
}
fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! {cx,
<Demo />
}
})
}

View file

@ -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::*;

247
src/use_event_listener.rs Normal file
View file

@ -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::<web_sys::HtmlDivElement>(&evt));
/// });
///
/// let (cond, set_cond) = create_signal(cx, true);
///
/// view! { cx,
/// <Show
/// when=move || cond()
/// fallback=move |cx| view! { cx, <div node_ref=element>"Condition false"</div> }
/// >
/// <div node_ref=element>"Condition true"</div>
/// </Show>
/// }
/// }
/// ```
///
/// 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<Ev, El, Inner, F>(
cx: Scope,
target: El,
event: Ev,
handler: F,
) -> Box<dyn Fn()>
where
Ev: EventDescriptor + 'static,
El: Into<MaybeSignal<Option<Inner>>>,
Inner: Into<web_sys::EventTarget> + Clone + 'static,
F: FnMut(<Ev as EventDescriptor>::EventType) + 'static,
{
let event_name = event.name();
let closure_js = Closure::wrap(Box::new(handler) as Box<dyn FnMut(_)>).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<dyn Fn()>))
} else {
Rc::new(RefCell::new(Box::new(move || {}) as Box<dyn Fn()>))
};
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<dyn Fn()>);
} else {
cleanup_prev_el.replace(Box::new(move || {}) as Box<dyn Fn()>);
}
});
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<Ev, El, F>(
cx: Scope,
target: NodeRef<El>,
event: Ev,
handler: F,
) -> Box<dyn Fn()>
where
Ev: EventDescriptor + 'static,
El: ElementDescriptor + Clone,
F: FnMut(<Ev as EventDescriptor>::EventType) + 'static,
{
let event_name = event.name();
let closure_js = Closure::wrap(Box::new(handler) as Box<dyn FnMut(_)>).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<dyn Fn()>))
} else {
Rc::new(RefCell::new(Box::new(move || {}) as Box<dyn Fn()>))
};
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<dyn Fn()>);
} else {
cleanup_prev_el.replace(Box::new(move || {}) as Box<dyn Fn()>);
}
});
let cleanup_fn = move || cleanup_prev_element.borrow()();
on_cleanup(cx, cleanup_fn.clone());
Box::new(cleanup_fn)
}