Skip to content

Commit

Permalink
Update Compose Refs to Leptos 0.7
Browse files Browse the repository at this point in the history
  • Loading branch information
geoffreygarrett committed Jan 6, 2025
1 parent 2ab3db1 commit 9445e62
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 10 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ members = [
"packages/primitives/leptos/accessible-icon",
"packages/primitives/leptos/arrow",
"packages/primitives/leptos/aspect-ratio",
"packages/primitives/leptos/compose-refs",
"packages/primitives/leptos/direction",
"packages/primitives/leptos/id",
"packages/primitives/leptos/label",
Expand Down Expand Up @@ -62,3 +63,4 @@ yew-style = "0.1.4"
[patch.crates-io]
yew = { git = "https://github.com/RustForWeb/yew.git", branch = "feature/use-composed-ref" }
yew-router = { git = "https://github.com/RustForWeb/yew.git", branch = "feature/use-composed-ref" }
leptos-node-ref = { git = "https://github.com/geoffreygarrett/leptos-utils", branch = "feature/any-node-ref" }
1 change: 1 addition & 0 deletions packages/primitives/leptos/compose-refs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ version.workspace = true

[dependencies]
leptos.workspace = true
leptos-node-ref.workspace = true
2 changes: 1 addition & 1 deletion packages/primitives/leptos/compose-refs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ See [the Rust Radix book](https://radix.rustforweb.org/) for documentation.

## Rust For Web

The Rust Radix project is part of [Rust For Web](https://github.com/RustForWeb).
The Rust Radix project is part of the [Rust For Web](https://github.com/RustForWeb).

[Rust For Web](https://github.com/RustForWeb) creates and ports web UI libraries for Rust. All projects are free and open source.
252 changes: 243 additions & 9 deletions packages/primitives/leptos/compose-refs/src/compose_refs.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,255 @@
use leptos::{html::ElementDescriptor, Effect, NodeRef};
use leptos::{
html::{self, ElementType},
prelude::*,
tachys::html::node_ref::NodeRefContainer,
wasm_bindgen::JsCast,
web_sys::Element,
};
use leptos_node_ref::prelude::*;
use std::{rc::Rc};

fn compose_refs<T: ElementDescriptor + Clone + 'static>(refs: Vec<NodeRef<T>>) -> NodeRef<T> {
let composed_ref = NodeRef::new();
/// A trait for composable node references that can be combined,
/// while maintaining static dispatch (tuples) and dynamic dispatch (iterables).
pub trait ComposeRefs {
/// Applies the composition to a given DOM node.
fn compose_with(&self, node: &Element);
}

// -------------------------------------
// 1. Static Implementations
// -------------------------------------

impl ComposeRefs for AnyNodeRef {
#[inline(always)]
fn compose_with(&self, node: &Element) {
<AnyNodeRef as NodeRefContainer<html::Div>>::load(*self, node);
}
}

impl<T> ComposeRefs for NodeRef<T>
where
T: ElementType,
T::Output: JsCast,
{
#[inline(always)]
fn compose_with(&self, node: &Element) {
<NodeRef<T> as NodeRefContainer<T>>::load(*self, node);
}
}

// NOTE: See macro ahead, replaces these. These are
// left for illustration for now.
// impl<A, B> ComposeRefs for (A, B)
// where
// A: ComposeRefs,
// B: ComposeRefs,
// {
// #[inline(always)]
// fn compose_with(&self, node: &Element) {
// self.0.compose_with(node);
// self.1.compose_with(node);
// }
// }

// impl<A, B, C> ComposeRefs for (A, B, C)
// where
// A: ComposeRefs,
// B: ComposeRefs,
// C: ComposeRefs,
// {
// #[inline(always)]
// fn compose_with(&self, node: &Element) {
// self.0.compose_with(node);
// self.1.compose_with(node);
// self.2.compose_with(node);
// }
// }

macro_rules! impl_compose_refs_tuple {
($($idx:tt $type:ident),+) => {
impl<$($type),+> ComposeRefs for ($($type),+)
where
$($type: ComposeRefs),+
{
#[inline(always)]
fn compose_with(&self, node: &Element) {
$(
self.$idx.compose_with(node);
)+
}
}
}
}

impl_compose_refs_tuple!(0 A, 1 B);
impl_compose_refs_tuple!(0 A, 1 B, 2 C);
impl_compose_refs_tuple!(0 A, 1 B, 2 C, 3 D);
impl_compose_refs_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
impl_compose_refs_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);

// -------------------------------------
// 2. Dynamic Implementations
// -------------------------------------

/// Implementation for arrays of any size
impl<T: ComposeRefs, const N: usize> ComposeRefs for [T; N] {
fn compose_with(&self, node: &Element) {
for item in self.iter() {
item.compose_with(node);
}
}
}

/// Implementation for slice references
impl<T: ComposeRefs> ComposeRefs for &[T] {
fn compose_with(&self, node: &Element) {
for item in (*self).iter() {
item.compose_with(node);
}
}
}

/// Implementation for Vec
impl<T: ComposeRefs> ComposeRefs for Vec<T> {
fn compose_with(&self, node: &Element) {
for item in self.iter() {
item.compose_with(node);
}
}
}

// -------------------------------------
// 3. compose_refs + Hook
// -------------------------------------

/// Combines multiple node references into a single reference that, when set,
/// updates all input references to point to the same DOM node.
///
/// - **Static**: Tuples (`(ref1, ref2, ...)`)—no heap allocation.
/// - **Dynamic**: Any iterable (`Vec`, slice, array) of references.
///
/// # Examples
/// ```rust
/// use leptos::{html::Div, html::Button};
/// use leptos::prelude::NodeRef;
/// use leptos_node_ref::prelude::*;
/// use radix_leptos_compose_refs::compose_refs;
///
/// // 1) Static composition (tuples):
/// let div_ref = NodeRef::<Div>::new();
/// let btn_ref = NodeRef::<Button>::new();
/// let composed = compose_refs((div_ref, btn_ref));
///
/// // 2) Dynamic composition (Vec, slice):
/// let refs = vec![div_ref.into_any(), btn_ref.into_any()];
/// let composed = compose_refs(refs);
/// ```
pub fn compose_refs<T>(refs: T) -> AnyNodeRef
where
T: ComposeRefs + 'static,
{
let composed_ref = AnyNodeRef::new();
let refs = Rc::new(refs);

// Effect will re-run if `composed_ref` changes.
Effect::new(move |_| {
if let Some(node) = composed_ref.get() {
for r#ref in &refs {
r#ref.load(&node);
}
refs.compose_with(&node);
}
});

composed_ref
}

pub fn use_composed_refs<T: ElementDescriptor + Clone + 'static>(
refs: Vec<NodeRef<T>>,
) -> NodeRef<T> {
/// Hook-style wrapper around `compose_refs`.
///
/// Identical behavior, just a `use_*` naming convention.
pub fn use_composed_refs<T>(refs: T) -> AnyNodeRef
where
T: ComposeRefs + 'static,
{
compose_refs(refs)
}

// -------------------------------------
// 4. Tests
// -------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use leptos::html;

#[test]
fn test_static_any_node_ref() {
let any_ref1 = AnyNodeRef::new();
let any_ref2 = AnyNodeRef::new();
let composed = compose_refs((any_ref1, any_ref2));
assert!(composed.get().is_none());
}

#[test]
fn test_static_specific_node_ref() {
let div_ref = NodeRef::<html::Div>::new();
let btn_ref = NodeRef::<html::Button>::new();
let composed = compose_refs((div_ref, btn_ref));
assert!(composed.get().is_none());
}

#[test]
fn test_triple_static() {
let r1 = NodeRef::<html::Div>::new();
let r2 = NodeRef::<html::Button>::new();
let r3 = AnyNodeRef::new();
let composed = compose_refs((r1, r2, r3));
assert!(composed.get().is_none());
}

#[test]
fn test_dynamic_vec_any_node_ref() {
let refs = vec![AnyNodeRef::new(), AnyNodeRef::new()];
let composed = compose_refs(refs);
assert!(composed.get().is_none());
}

#[test]
fn test_dynamic_vec_specific_node_ref() {
let div_ref = NodeRef::<html::Div>::new();
let btn_ref = NodeRef::<html::Button>::new();
let refs = vec![div_ref.into_any(), btn_ref.into_any()];
let composed = compose_refs(refs);
assert!(composed.get().is_none());
}

#[test]
fn test_array_of_any_node_ref() {
let arr = [AnyNodeRef::new(), AnyNodeRef::new()];
let composed = compose_refs(arr);
assert!(composed.get().is_none());
}

#[test]
fn test_slice_of_any_node_ref() {
let vec_refs = vec![AnyNodeRef::new(), AnyNodeRef::new()];
let composed = compose_refs(vec_refs);
assert!(composed.get().is_none());
}

#[test]
fn test_compositions() {
// Array
let arr = [AnyNodeRef::new(), AnyNodeRef::new()];
let composed = compose_refs(arr);
assert!(composed.get().is_none());

// Owned Vec instead of slice
let vec_refs = vec![AnyNodeRef::new(), AnyNodeRef::new()];
let composed = compose_refs(vec_refs);
assert!(composed.get().is_none());

// Vec
let v = vec![AnyNodeRef::new(), AnyNodeRef::new()];
let composed = compose_refs(v);
assert!(composed.get().is_none());
}

}

0 comments on commit 9445e62

Please sign in to comment.