leptos_node_ref/
any_node_ref.rs

1use std::marker::PhantomData;
2
3use leptos::{
4    attr::{Attribute, NextAttribute},
5    html::ElementType,
6    prelude::{
7        DefinedAt, Get, NodeRef, ReadUntracked, RwSignal, Set, Track,
8        guards::{Derefable, ReadGuard},
9    },
10    tachys::{html::node_ref::NodeRefContainer, renderer::types::Element},
11};
12use send_wrapper::SendWrapper;
13
14/// A reactive reference to a DOM node that can be used with the `node_ref` attribute.
15#[derive(Debug)]
16pub struct AnyNodeRef(RwSignal<Option<SendWrapper<Element>>>);
17
18impl AnyNodeRef {
19    /// Creates a new [`AnyNodeRef`].
20    #[track_caller]
21    pub fn new() -> Self {
22        Self(RwSignal::new(None))
23    }
24}
25
26impl Default for AnyNodeRef {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl Clone for AnyNodeRef {
33    fn clone(&self) -> Self {
34        *self
35    }
36}
37
38impl Copy for AnyNodeRef {}
39
40impl DefinedAt for AnyNodeRef {
41    fn defined_at(&self) -> Option<&'static std::panic::Location<'static>> {
42        self.0.defined_at()
43    }
44}
45
46impl<T: ElementType> From<NodeRef<T>> for AnyNodeRef
47where
48    NodeRef<T>: IntoAnyNodeRef,
49{
50    fn from(value: NodeRef<T>) -> Self {
51        value.into_any()
52    }
53}
54
55impl<E: ElementType> NodeRefContainer<E> for AnyNodeRef {
56    fn load(self, el: &Element) {
57        self.0.set(Some(SendWrapper::new(el.clone())));
58    }
59}
60
61impl ReadUntracked for AnyNodeRef {
62    type Value = ReadGuard<Option<Element>, Derefable<Option<Element>>>;
63
64    fn try_read_untracked(&self) -> Option<Self::Value> {
65        Some(ReadGuard::new(Derefable(
66            self.0.try_read_untracked()?.as_deref().cloned(),
67        )))
68    }
69}
70
71impl Track for AnyNodeRef {
72    fn track(&self) {
73        self.0.track();
74    }
75}
76
77/// Allows converting any node reference into our type-erased [`AnyNodeRef`].
78pub trait IntoAnyNodeRef {
79    /// Converts `self` into an [`AnyNodeRef`].
80    fn into_any(self) -> AnyNodeRef;
81}
82
83impl<E> IntoAnyNodeRef for NodeRef<E>
84where
85    E: ElementType,
86    E::Output: AsRef<Element>,
87    NodeRef<E>: Get<Value = Option<E::Output>>,
88{
89    fn into_any(self) -> AnyNodeRef {
90        let any_ref = AnyNodeRef::new();
91        if let Some(element) = self.get() {
92            NodeRefContainer::<E>::load(any_ref, element.as_ref());
93        }
94        any_ref
95    }
96}
97
98impl IntoAnyNodeRef for AnyNodeRef {
99    fn into_any(self) -> AnyNodeRef {
100        self
101    }
102}
103
104/// Attribute wrapper for node references that allows conditional rendering across elements.
105///
106/// Useful when distributing node references across multiple rendering branches.
107#[derive(Debug)]
108pub struct AnyNodeRefAttr<E, C> {
109    container: C,
110    ty: PhantomData<E>,
111}
112
113impl<E, C> Clone for AnyNodeRefAttr<E, C>
114where
115    C: Clone,
116{
117    fn clone(&self) -> Self {
118        Self {
119            container: self.container.clone(),
120            ty: PhantomData,
121        }
122    }
123}
124
125impl<E, C> Attribute for AnyNodeRefAttr<E, C>
126where
127    E: ElementType + 'static,
128    C: NodeRefContainer<E> + Clone + 'static,
129    Element: PartialEq,
130{
131    const MIN_LENGTH: usize = 0;
132    type State = Element;
133    type AsyncOutput = Self;
134    type Cloneable = Self;
135    type CloneableOwned = Self;
136
137    #[inline(always)]
138    fn html_len(&self) -> usize {
139        0
140    }
141
142    fn to_html(
143        self,
144        _buf: &mut String,
145        _class: &mut String,
146        _style: &mut String,
147        _inner_html: &mut String,
148    ) {
149    }
150
151    fn hydrate<const FROM_SERVER: bool>(self, el: &Element) -> Self::State {
152        self.container.load(el);
153        el.clone()
154    }
155
156    fn build(self, el: &Element) -> Self::State {
157        self.container.load(el);
158        el.clone()
159    }
160
161    fn rebuild(self, state: &mut Self::State) {
162        self.container.load(state);
163    }
164
165    fn into_cloneable(self) -> Self::Cloneable {
166        self
167    }
168
169    fn into_cloneable_owned(self) -> Self::CloneableOwned {
170        self
171    }
172
173    fn dry_resolve(&mut self) {}
174
175    async fn resolve(self) -> Self::AsyncOutput {
176        self
177    }
178}
179
180impl<E, C> NextAttribute for AnyNodeRefAttr<E, C>
181where
182    E: ElementType + 'static,
183    C: NodeRefContainer<E> + Clone + 'static,
184    Element: PartialEq,
185{
186    type Output<NewAttr: Attribute> = (Self, NewAttr);
187
188    fn add_any_attr<NewAttr: Attribute>(self, new_attr: NewAttr) -> Self::Output<NewAttr> {
189        (self, new_attr)
190    }
191}
192
193/// Constructs an attribute to attach an [`AnyNodeRef`] to an element.
194///
195/// Enables adding node references in conditional/dynamic rendering branches.
196pub fn any_node_ref<E, C>(container: C) -> AnyNodeRefAttr<E, C>
197where
198    E: ElementType,
199    C: NodeRefContainer<E>,
200{
201    AnyNodeRefAttr {
202        container,
203        ty: PhantomData,
204    }
205}
206
207pub mod prelude {
208    pub use super::*;
209    pub use AnyNodeRef;
210    pub use IntoAnyNodeRef;
211    pub use any_node_ref;
212}
213
214#[cfg(test)]
215mod tests {
216    use leptos::{html, prelude::*};
217
218    use super::{any_node_ref, prelude::*};
219
220    #[test]
221    fn test_any_node_ref_creation() {
222        let node_ref = AnyNodeRef::new();
223        assert!(node_ref.get().is_none(), "New AnyNodeRef should be empty");
224    }
225
226    #[test]
227    fn test_to_any_node_ref() {
228        let div_ref: NodeRef<html::Div> = NodeRef::new();
229        let any_ref = div_ref.into_any();
230        assert!(
231            any_ref.get().is_none(),
232            "Converted AnyNodeRef should be initially empty"
233        );
234    }
235
236    #[test]
237    fn test_clone_and_copy() {
238        let node_ref = AnyNodeRef::new();
239        let cloned_ref = node_ref;
240        let _copied_ref = cloned_ref; // Should be copyable
241        assert!(
242            cloned_ref.get().is_none(),
243            "Cloned AnyNodeRef should be empty"
244        );
245    }
246
247    #[test]
248    fn test_default() {
249        let node_ref = AnyNodeRef::default();
250        assert!(
251            node_ref.get().is_none(),
252            "Default AnyNodeRef should be empty"
253        );
254    }
255
256    #[test]
257    fn test_into_any_node_ref_trait() {
258        let div_ref: NodeRef<html::Div> = NodeRef::new();
259        let _any_ref: AnyNodeRef = div_ref.into_any();
260
261        let input_ref: NodeRef<html::Input> = NodeRef::new();
262        let _any_input_ref: AnyNodeRef = input_ref.into_any();
263    }
264
265    #[test]
266    fn test_from_node_ref() {
267        let div_ref: NodeRef<html::Div> = NodeRef::new();
268        let _any_ref: AnyNodeRef = div_ref.into();
269    }
270
271    #[test]
272    fn test_any_node_ref_attr() {
273        let node_ref = AnyNodeRef::new();
274        let _attr = any_node_ref::<html::Div, _>(node_ref);
275    }
276
277    #[test]
278    fn test_defined_at() {
279        let node_ref = AnyNodeRef::new();
280        assert!(node_ref.defined_at().is_some());
281    }
282
283    #[test]
284    fn test_track_and_untracked() {
285        let node_ref = AnyNodeRef::new();
286        // Just testing that these don't panic
287        node_ref.track();
288        let _untracked = node_ref.try_read_untracked();
289    }
290
291    #[test]
292    fn test_into_any_identity() {
293        let node_ref = AnyNodeRef::new();
294        let same_ref = node_ref.into_any();
295
296        // Instead of checking pointer equality, we should verify:
297        // 1. Both refs are initially empty
298        assert!(node_ref.get().is_none());
299        assert!(same_ref.get().is_none());
300
301        // 2. When we set one, both should reflect the change
302        // (This would require a mock Element to test properly)
303
304        // 3. They should have the same defined_at location
305        assert_eq!(node_ref.defined_at(), same_ref.defined_at());
306    }
307}