From 84396b767591fea2fdfa9da53e6d2a37d07899da Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 10 Jan 2024 23:37:32 +0000 Subject: [PATCH] implemented various conversions for Element(s)MaybeSignal closes #48 --- CHANGELOG.md | 2 + docs/book/src/SUMMARY.md | 1 + docs/book/src/element_parameters.md | 95 ++++++++ examples/ssr/src/main.rs | 2 +- src/core/element_maybe_signal.rs | 65 +++-- src/core/elements_maybe_signal.rs | 354 ++++++++++++++++++++++++---- src/on_click_outside.rs | 45 +++- 7 files changed, 503 insertions(+), 61 deletions(-) create mode 100644 docs/book/src/element_parameters.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 5881465..8249bce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changes 🔥 - The `UseMouseReturn` signals `x`, `y`, and `source_type` are now of type `Signal` instead of `ReadSignal`. +- You can now convert `leptos::html::HtmlElement` into `Element(s)MaybeSignal`. This should make functions a lot easier to use in directives. +- There's now a chapter in the book especially for `Element(s)MaybeSignal`. ## [0.9.0] - 2023-12-06 diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index b7bda4a..87701a3 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -2,6 +2,7 @@ [Introduction](introduction.md) [Get Started](get_started.md) +[Element Parameters](element_parameters.md) [Server-Side Rendering](server_side_rendering.md) [Changelog](changelog.md) [Functions](functions.md) diff --git a/docs/book/src/element_parameters.md b/docs/book/src/element_parameters.md new file mode 100644 index 0000000..086e48c --- /dev/null +++ b/docs/book/src/element_parameters.md @@ -0,0 +1,95 @@ +# Element Parameters + +Many functions in this library operate on HTML/SVG elements. For example, the +function [`use_element_size`](elements/use_element_size.md) returns the width and height of an element: + +```rust +# use leptos::{*, html::Div}; +# use leptos_use::{use_element_size, UseElementSizeReturn}; +# +# #[component] +# pub fn Component() -> impl IntoView { +let el = create_node_ref::
(); + +let UseElementSizeReturn { width, height } = use_element_size(el); + +view! { +
+} +# } +``` + +In the example above we used a Leptos `NodeRef` to pass into the function. But that is not +the only way you can do that. All of these work as well: + +```rust +use_element_size(window().body()); // Option +use_element_size(window().body().unwrap()); // web_sys::Element +use_element_size("div > p.some-class"); // &str or String intepreted as CSS selector + +pub fn some_directive(el: HtmlElement) { + use_element_size(el); // leptos::html::HtmlElement +} +``` + +Signal of Strings: `Signal`, `ReadSignal`, `RwSignal`, `Memo`; also works with `&str`: + +```rust +let (str_signal, set_str_signal) = create_signal("div > p.some-class".to_string()); +use_element_size(str_signal); +``` + +Signals of Elements: `Signal`, `ReadSignal`, `RwSignal`, `Memo`; also works with `Option`: + +```rust +let (el_signal, set_el_signal) = create_signal(document().query_selector("div > p.some-class").unwrap()); +use_element_size(el_signal); +``` + +## How it works + +Looking at the source code of `use_element_size` you'll find sth like + +```rust +pub fn use_element_size(el: Into>) -> UseElementSizeReturn {} +``` + +All the above code works because there are `From` implementations for all of these +types for `ElementMaybeSignal`. + +## `ElementsMaybeSignal` + +Some functions work on one or more elements. Take [`use_resize_observer`](elements/use_resize_observer.md) for example. +This works very much the same way as described above but instead of `Into` +it takes an `Into` (note the plural). This means you can use it exactly in +the same ways as you saw with the singular `ElementMaybeSignal`. Only this time, when you use +`String` or `&str` it will be interpreted as CSS selector with `query_selector_all`. + +But you can also use it with containers. + +```rust +// Array of Option +use_resize_observer([window().body(), document().query_selector("div > p.some-class").unsrap()]); + +// Vec of &str. All of them will be interpreted as CSS selectors with query_selector_all() and the +// results will be merged into one Vec. +use_resize_observer(vec!["div > p.some-class", "p.some-class"]); + +// Slice of NodeRef +let node_ref1 = create_node_ref::
(); +let node_ref2 = create_node_ref::
(); +use_resize_observer(vec![node_ref1, node_ref2].as_slice()); +``` + +## Usage in Options + +Some functions have options that take `Element(s)MaybeSignal`. +They can be used in the same way. + +```rust +use_mouse_with_options( + UseMouseOptions::default().target("div > p.some-class") +); +``` + +See also ["Excluding Elements" in `on_click_outside`](elements/on_click_outside.md#excluding-elements). \ No newline at end of file diff --git a/examples/ssr/src/main.rs b/examples/ssr/src/main.rs index c08d00e..da34a9f 100644 --- a/examples/ssr/src/main.rs +++ b/examples/ssr/src/main.rs @@ -8,7 +8,7 @@ async fn main() { 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"); + simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging"); // Setting get_configuration(None) means we'll be using cargo-leptos's env values // For deployment these variables are: diff --git a/src/core/element_maybe_signal.rs b/src/core/element_maybe_signal.rs index a100b7e..47c08e6 100644 --- a/src/core/element_maybe_signal.rs +++ b/src/core/element_maybe_signal.rs @@ -1,6 +1,6 @@ use crate::{UseDocument, UseWindow}; use cfg_if::cfg_if; -use leptos::html::ElementDescriptor; +use leptos::html::{ElementDescriptor, HtmlElement}; use leptos::*; use std::marker::PhantomData; use std::ops::Deref; @@ -196,23 +196,37 @@ where } } -impl From> for ElementMaybeSignal -where - E: From + 'static, -{ - fn from(signal: Signal) -> Self { - cfg_if! { if #[cfg(feature = "ssr")] { - let _ = signal; - Self::Dynamic(Signal::derive(|| None)) - } else { - Self::Dynamic( - create_memo(move |_| document().query_selector(&signal.get()).unwrap_or_default()) - .into(), - ) - }} - } +macro_rules! impl_from_signal_string { + ($ty:ty) => { + impl From<$ty> for ElementMaybeSignal + where + E: From + 'static, + { + fn from(signal: $ty) -> Self { + cfg_if! { if #[cfg(feature = "ssr")] { + let _ = signal; + Self::Dynamic(Signal::derive(|| None)) + } else { + Self::Dynamic( + create_memo(move |_| document().query_selector(&signal.get()).unwrap_or_default()) + .into(), + ) + }} + } + } + }; } +impl_from_signal_string!(Signal); +impl_from_signal_string!(ReadSignal); +impl_from_signal_string!(RwSignal); +impl_from_signal_string!(Memo); + +impl_from_signal_string!(Signal<&str>); +impl_from_signal_string!(ReadSignal<&str>); +impl_from_signal_string!(RwSignal<&str>); +impl_from_signal_string!(Memo<&str>); + // From signal /////////////////////////////////////////////////////////////// macro_rules! impl_from_signal_option { @@ -274,3 +288,22 @@ macro_rules! impl_from_node_ref { impl_from_node_ref!(web_sys::EventTarget); impl_from_node_ref!(web_sys::Element); + +// From leptos::html::HTMLElement /////////////////////////////////////////////// + +macro_rules! impl_from_html_element { + ($ty:ty) => { + impl From> for ElementMaybeSignal<$ty, $ty> + where + HtmlEl: ElementDescriptor + std::ops::Deref, + { + fn from(value: HtmlElement) -> Self { + let el: &$ty = value.deref(); + Self::Static(Some(el.clone())) + } + } + }; +} + +impl_from_html_element!(web_sys::EventTarget); +impl_from_html_element!(web_sys::Element); diff --git a/src/core/elements_maybe_signal.rs b/src/core/elements_maybe_signal.rs index 2770d48..63cbad6 100644 --- a/src/core/elements_maybe_signal.rs +++ b/src/core/elements_maybe_signal.rs @@ -180,6 +180,9 @@ where { fn from(target: &'a str) -> Self { cfg_if! { if #[cfg(feature = "ssr")] { + let _ = target; + Self::Static(vec![]) + } else { 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() { @@ -191,9 +194,6 @@ where } else { Self::Static(vec![]) } - } else { - let _ = target; - Self::Static(vec![]) }} } } @@ -207,34 +207,48 @@ where } } -impl From> for ElementsMaybeSignal -where - E: From + 'static, -{ - fn from(signal: Signal) -> Self { - 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![] - } - }) - .into(), - ) - } else { - let _ = signal; - Self::Dynamic(Signal::derive(Vec::new)) - }} - } +macro_rules! impl_from_signal_string { + ($ty:ty) => { + impl From<$ty> for ElementsMaybeSignal + where + E: From + 'static, + { + fn from(signal: $ty) -> Self { + 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![] + } + }) + .into(), + ) + } else { + let _ = signal; + Self::Dynamic(Signal::derive(Vec::new)) + }} + } + } + }; } +impl_from_signal_string!(Signal); +impl_from_signal_string!(ReadSignal); +impl_from_signal_string!(RwSignal); +impl_from_signal_string!(Memo); + +impl_from_signal_string!(Signal<&str>); +impl_from_signal_string!(ReadSignal<&str>); +impl_from_signal_string!(RwSignal<&str>); +impl_from_signal_string!(Memo<&str>); + // From single signal /////////////////////////////////////////////////////////////// macro_rules! impl_from_signal_option { @@ -297,7 +311,26 @@ macro_rules! impl_from_node_ref { impl_from_node_ref!(web_sys::EventTarget); impl_from_node_ref!(web_sys::Element); -// From multiple static elements ////////////////////////////////////////////////////////////// +// From single leptos::html::HTMLElement /////////////////////////////////////////// + +macro_rules! impl_from_html_element { + ($ty:ty) => { + impl From> for ElementsMaybeSignal<$ty, $ty> + where + HtmlEl: ElementDescriptor + std::ops::Deref, + { + fn from(value: HtmlElement) -> Self { + let el: &$ty = value.deref(); + Self::Static(vec![Some(el.clone())]) + } + } + }; +} + +impl_from_html_element!(web_sys::EventTarget); +impl_from_html_element!(web_sys::Element); + +// From multiple static elements ////////////////////////////////////////////////////// impl From<&[T]> for ElementsMaybeSignal where @@ -317,6 +350,105 @@ where } } +impl From> for ElementsMaybeSignal +where + T: Into + Clone + 'static, +{ + fn from(target: Vec) -> Self { + Self::Static(target.iter().map(|t| Some(t.clone())).collect()) + } +} + +impl From>> for ElementsMaybeSignal +where + T: Into + Clone + 'static, +{ + fn from(target: Vec>) -> Self { + Self::Static(target.to_vec()) + } +} + +impl From<[T; C]> for ElementsMaybeSignal +where + T: Into + Clone + 'static, +{ + fn from(target: [T; C]) -> Self { + Self::Static(target.iter().map(|t| Some(t.clone())).collect()) + } +} + +impl From<[Option; C]> for ElementsMaybeSignal +where + T: Into + Clone + 'static, +{ + fn from(target: [Option; C]) -> Self { + Self::Static(target.to_vec()) + } +} + +// From multiple strings ////////////////////////////////////////////////////// + +macro_rules! impl_from_strings_inner { + ($el_ty:ty, $str_ty:ty, $target:ident) => { + Self::Static( + $target + .iter() + .filter_map(|sel: &$str_ty| -> Option>> { + cfg_if! { if #[cfg(feature = "ssr")] { + let _ = sel; + None + } else { + if let Ok(node_list) = document().query_selector_all(sel) { + let mut list = Vec::with_capacity(node_list.length() as usize); + for i in 0..node_list.length() { + let node: $el_ty = node_list.get(i).expect("checked the range").unchecked_into(); + list.push(Some(node)); + } + + Some(list) + } else { + None + } + }} + }) + .flatten() + .collect(), + ) + }; +} + +macro_rules! impl_from_strings_with_container { + ($el_ty:ty, $str_ty:ty, $container_ty:ty) => { + impl From<$container_ty> for ElementsMaybeSignal<$el_ty, $el_ty> { + fn from(target: $container_ty) -> Self { + impl_from_strings_inner!($el_ty, $str_ty, target) + } + } + }; +} + +macro_rules! impl_from_strings { + ($el_ty:ty, $str_ty:ty) => { + impl_from_strings_with_container!($el_ty, $str_ty, Vec<$str_ty>); + impl_from_strings_with_container!($el_ty, $str_ty, &[$str_ty]); + impl From<[$str_ty; C]> for ElementsMaybeSignal<$el_ty, $el_ty> { + fn from(target: [$str_ty; C]) -> Self { + impl_from_strings_inner!($el_ty, $str_ty, target) + } + } + impl From<&[$str_ty; C]> for ElementsMaybeSignal<$el_ty, $el_ty> { + fn from(target: &[$str_ty; C]) -> Self { + impl_from_strings_inner!($el_ty, $str_ty, target) + } + } + }; +} + +impl_from_strings!(web_sys::Element, &str); +impl_from_strings!(web_sys::Element, String); +impl_from_strings!(web_sys::EventTarget, &str); +impl_from_strings!(web_sys::EventTarget, String); + // From signal of vec //////////////////////////////////////////////////////////////// impl From>> for ElementsMaybeSignal @@ -367,8 +499,77 @@ where } } +impl From>> for ElementsMaybeSignal +where + T: Into + Clone + 'static, +{ + fn from(list: Vec>) -> Self { + let list = list.clone(); + + Self::Dynamic(Signal::derive(move || { + list.iter().map(|t| Some(t.get())).collect() + })) + } +} + +impl From>>> for ElementsMaybeSignal +where + T: Into + Clone + 'static, +{ + fn from(list: Vec>>) -> Self { + let list = list.clone(); + + Self::Dynamic(Signal::derive(move || { + list.iter().map(|t| t.get()).collect() + })) + } +} + +impl From<[Signal; C]> for ElementsMaybeSignal +where + T: Into + Clone + 'static, +{ + fn from(list: [Signal; C]) -> Self { + let list = list.to_vec(); + + Self::Dynamic(Signal::derive(move || { + list.iter().map(|t| Some(t.get())).collect() + })) + } +} + +impl From<[Signal>; C]> for ElementsMaybeSignal +where + T: Into + Clone + 'static, +{ + fn from(list: [Signal>; C]) -> Self { + let list = list.to_vec(); + + Self::Dynamic(Signal::derive(move || { + list.iter().map(|t| t.get()).collect() + })) + } +} + // From multiple NodeRefs ////////////////////////////////////////////////////////////// +macro_rules! impl_from_multi_node_ref_inner { + ($ty:ty, $node_refs:ident) => { + Self::Dynamic(Signal::derive(move || { + $node_refs + .iter() + .map(|node_ref| { + node_ref.get().map(move |el| { + let el = el.into_any(); + let el: $ty = el.deref().clone().into(); + el + }) + }) + .collect() + })) + }; +} + macro_rules! impl_from_multi_node_ref { ($ty:ty) => { impl From<&[NodeRef]> for ElementsMaybeSignal<$ty, $ty> @@ -377,19 +578,27 @@ macro_rules! impl_from_multi_node_ref { { fn from(node_refs: &[NodeRef]) -> Self { let node_refs = node_refs.to_vec(); + impl_from_multi_node_ref_inner!($ty, node_refs) + } + } - Self::Dynamic(Signal::derive(move || { - node_refs - .iter() - .map(|node_ref| { - node_ref.get().map(move |el| { - let el = el.into_any(); - let el: $ty = el.deref().clone().into(); - el - }) - }) - .collect() - })) + impl From<[NodeRef; C]> for ElementsMaybeSignal<$ty, $ty> + where + R: ElementDescriptor + Clone + 'static, + { + fn from(node_refs: [NodeRef; C]) -> Self { + let node_refs = node_refs.to_vec(); + impl_from_multi_node_ref_inner!($ty, node_refs) + } + } + + impl From>> for ElementsMaybeSignal<$ty, $ty> + where + R: ElementDescriptor + Clone + 'static, + { + fn from(node_refs: Vec>) -> Self { + let node_refs = node_refs.clone(); + impl_from_multi_node_ref_inner!($ty, node_refs) } } }; @@ -398,6 +607,67 @@ macro_rules! impl_from_multi_node_ref { impl_from_multi_node_ref!(web_sys::EventTarget); impl_from_multi_node_ref!(web_sys::Element); +// From multiple leptos::html::HTMLElement ///////////////////////////////////////// + +macro_rules! impl_from_multi_html_element { + ($ty:ty) => { + impl From<&[HtmlElement]> for ElementsMaybeSignal<$ty, $ty> + where + HtmlEl: ElementDescriptor + std::ops::Deref, + { + fn from(value: &[HtmlElement]) -> Self { + Self::Static( + value + .iter() + .map(|el| { + let el: &$ty = el.deref(); + Some(el.clone()) + }) + .collect(), + ) + } + } + + impl From<[HtmlElement; C]> + for ElementsMaybeSignal<$ty, $ty> + where + HtmlEl: ElementDescriptor + std::ops::Deref, + { + fn from(value: [HtmlElement; C]) -> Self { + Self::Static( + value + .iter() + .map(|el| { + let el: &$ty = el.deref(); + Some(el.clone()) + }) + .collect(), + ) + } + } + + impl From>> for ElementsMaybeSignal<$ty, $ty> + where + HtmlEl: ElementDescriptor + std::ops::Deref, + { + fn from(value: Vec>) -> Self { + Self::Static( + value + .iter() + .map(|el| { + let el: &$ty = el.deref(); + Some(el.clone()) + }) + .collect(), + ) + } + } + }; +} + +impl_from_multi_html_element!(web_sys::EventTarget); +impl_from_multi_html_element!(web_sys::Element); + // From ElementMaybeSignal ////////////////////////////////////////////////////////////// impl From> for ElementsMaybeSignal diff --git a/src/on_click_outside.rs b/src/on_click_outside.rs index 3d1a542..eddb6bf 100644 --- a/src/on_click_outside.rs +++ b/src/on_click_outside.rs @@ -27,7 +27,6 @@ cfg_if! { if #[cfg(not(feature = "ssr"))] { /// /// ``` /// # use leptos::*; -/// # use leptos::ev::resize; /// # use leptos::logging::log; /// # use leptos::html::Div; /// # use leptos_use::on_click_outside; @@ -50,6 +49,33 @@ cfg_if! { if #[cfg(not(feature = "ssr"))] { /// If you are targeting these browsers, we recommend you to include /// [this code snippet](https://gist.github.com/sibbng/13e83b1dd1b733317ce0130ef07d4efd) on your project. /// +/// ## Excluding Elements +/// +/// Use this to ignore clicks on certain elements. +/// +/// ``` +/// # use leptos::*; +/// # use leptos::logging::log; +/// # use leptos::html::Div; +/// # use leptos_use::{on_click_outside_with_options, OnClickOutsideOptions}; +/// # +/// # #[component] +/// # fn Demo() -> impl IntoView { +/// # let target = create_node_ref::
(); +/// # +/// on_click_outside_with_options( +/// target, +/// move |event| { log!("{:?}", event); }, +/// OnClickOutsideOptions::default().ignore(["input", "#some-id"]), +/// ); +/// # +/// # view! { +/// #
"Hello World"
+/// # } +/// # } +/// +/// ``` +/// /// ## Server-Side Rendering /// /// On the server this amounts to a no-op. @@ -230,12 +256,13 @@ where /// Options for [`on_click_outside_with_options`]. #[derive(Clone, DefaultBuilder)] +#[cfg_attr(feature = "ssr", allow(dead_code))] pub struct OnClickOutsideOptions where T: Into + Clone + 'static, { /// List of elementss that should not trigger the callback. Defaults to `[]`. - #[cfg_attr(feature = "ssr", allow(dead_code))] + #[builder(skip)] ignore: ElementsMaybeSignal, /// Use capturing phase for internal event listener. Defaults to `true`. @@ -257,3 +284,17 @@ where } } } + +impl OnClickOutsideOptions +where + T: Into + Clone + 'static, +{ + /// List of elementss that should not trigger the callback. Defaults to `[]`. + #[cfg_attr(feature = "ssr", allow(dead_code))] + pub fn ignore(self, ignore: impl Into>) -> Self { + Self { + ignore: ignore.into(), + ..self + } + } +}