diff --git a/packages/leptos-node-ref/src/any_node_ref.rs b/packages/leptos-node-ref/src/any_node_ref.rs index 4472afe..17becb0 100644 --- a/packages/leptos-node-ref/src/any_node_ref.rs +++ b/packages/leptos-node-ref/src/any_node_ref.rs @@ -1,7 +1,11 @@ +use std::marker::PhantomData; + use leptos::{ + attr::{Attribute, NextAttribute}, + html::ElementType, prelude::{ guards::{Derefable, ReadGuard}, - DefinedAt, ReadUntracked, RwSignal, Set, Track, + DefinedAt, Get, NodeRef, ReadUntracked, RwSignal, Set, Track, }, tachys::{html::node_ref::NodeRefContainer, renderer::types::Element}, }; @@ -12,7 +16,7 @@ use send_wrapper::SendWrapper; pub struct AnyNodeRef(RwSignal>>); impl AnyNodeRef { - /// Creates a new node reference. + /// Creates a new [`AnyNodeRef`]. #[track_caller] pub fn new() -> Self { Self(RwSignal::new(None)) @@ -39,6 +43,21 @@ impl DefinedAt for AnyNodeRef { } } +impl From> for AnyNodeRef +where + NodeRef: IntoAnyNodeRef, +{ + fn from(value: NodeRef) -> Self { + value.into_any() + } +} + +impl NodeRefContainer for AnyNodeRef { + fn load(self, el: &Element) { + self.0.set(Some(SendWrapper::new(el.clone()))); + } +} + impl ReadUntracked for AnyNodeRef { type Value = ReadGuard, Derefable>>; @@ -55,151 +74,234 @@ impl Track for AnyNodeRef { } } -macro_rules! impl_html_any_node_ref { - ($($element:ident),*,) => { - $(impl NodeRefContainer for AnyNodeRef { - fn load(self, el: &Element) { - // safe to construct SendWrapper here, because it will only run in the browser - // so it will always be accessed or dropped from the main thread - self.0.set(Some(SendWrapper::new(el.clone()))); - } - })* - }; -} - -macro_rules! impl_math_any_node_ref { - ($($element:ident),*,) => { - $(impl NodeRefContainer for AnyNodeRef { - fn load(self, el: &Element) { - // safe to construct SendWrapper here, because it will only run in the browser - // so it will always be accessed or dropped from the main thread - self.0.set(Some(SendWrapper::new(el.clone()))); - } - })* - }; -} - -macro_rules! impl_svg_any_node_ref { - ($($element:ident),*,) => { - $(impl NodeRefContainer for AnyNodeRef { - fn load(self, el: &Element) { - // safe to construct SendWrapper here, because it will only run in the browser - // so it will always be accessed or dropped from the main thread - self.0.set(Some(SendWrapper::new(el.clone()))); - } - })* - }; -} - -impl_html_any_node_ref!( - A, Abbr, Address, Area, Article, Aside, Audio, B, Base, Bdi, Bdo, Blockquote, Body, Br, Button, - Canvas, Caption, Cite, Code, Col, Colgroup, Data, Datalist, Dd, Del, Details, Dfn, Dialog, Div, - Dl, Dt, Em, Embed, Fieldset, Figcaption, Figure, Footer, Form, H1, H2, H3, H4, H5, H6, Head, - Header, Hgroup, Hr, Html, I, Iframe, Img, Input, Ins, Kbd, Label, Legend, Li, Link, Main, Map, - Mark, Menu, Meta, Meter, Nav, Noscript, Object, Ol, Optgroup, Option_, Output, P, Picture, - Portal, Pre, Progress, Q, Rp, Rt, Ruby, S, Samp, Script, Search, Section, Select, Slot, Small, - Source, Span, Strong, Style, Sub, Summary, Sup, Table, Tbody, Td, Template, Textarea, Tfoot, - Th, Thead, Time, Title, Tr, Track, U, Ul, Var, Video, Wbr, -); - -impl_math_any_node_ref!( - Math, - Mi, - Mn, - Mo, - Ms, - Mspace, - Mtext, - Menclose, - Merror, - Mfenced, - Mfrac, - Mpadded, - Mphantom, - Mroot, - Mrow, - Msqrt, - Mstyle, - Mmultiscripts, - Mover, - Mprescripts, - Msub, - Msubsup, - Msup, - Munder, - Munderover, - Mtable, - Mtd, - Mtr, - Maction, - Annotation, - Semantics, -); - -impl_svg_any_node_ref!( - A, - Animate, - AnimateMotion, - AnimateTransform, - Circle, - ClipPath, - Defs, - Desc, - Discard, - Ellipse, - FeBlend, - FeColorMatrix, - FeComponentTransfer, - FeComposite, - FeConvolveMatrix, - FeDiffuseLighting, - FeDisplacementMap, - FeDistantLight, - FeDropShadow, - FeFlood, - FeFuncA, - FeFuncB, - FeFuncG, - FeFuncR, - FeGaussianBlur, - FeImage, - FeMerge, - FeMergeNode, - FeMorphology, - FeOffset, - FePointLight, - FeSpecularLighting, - FeSpotLight, - FeTile, - FeTurbulence, - Filter, - ForeignObject, - G, - Hatch, - Hatchpath, - Image, - Line, - LinearGradient, - Marker, - Mask, - Metadata, - Mpath, - Path, - Pattern, - Polygon, - Polyline, - RadialGradient, - Rect, - Script, - Set, - Stop, - Style, - Svg, - Switch, - Symbol, - Text, - TextPath, - Title, - Tspan, - View, -); +/// Allows converting any node reference into our type-erased [`AnyNodeRef`]. +pub trait IntoAnyNodeRef { + /// Converts `self` into an [`AnyNodeRef`]. + fn into_any(self) -> AnyNodeRef; +} + +impl IntoAnyNodeRef for NodeRef +where + E: ElementType, + E::Output: AsRef, + NodeRef: Get>, +{ + fn into_any(self) -> AnyNodeRef { + let any_ref = AnyNodeRef::new(); + if let Some(element) = self.get() { + NodeRefContainer::::load(any_ref, element.as_ref()); + } + any_ref + } +} + +impl IntoAnyNodeRef for AnyNodeRef { + fn into_any(self) -> AnyNodeRef { + self + } +} + +/// Attribute wrapper for node references that allows conditional rendering across elements. +/// +/// Useful when distributing node references across multiple rendering branches. +#[derive(Debug)] +pub struct AnyNodeRefAttr { + container: C, + ty: PhantomData, +} + +impl Clone for AnyNodeRefAttr +where + C: Clone, +{ + fn clone(&self) -> Self { + Self { + container: self.container.clone(), + ty: PhantomData, + } + } +} + +impl Attribute for AnyNodeRefAttr +where + E: ElementType + 'static, + C: NodeRefContainer + Clone + 'static, + Element: PartialEq, +{ + const MIN_LENGTH: usize = 0; + type State = Element; + type AsyncOutput = Self; + type Cloneable = Self; + type CloneableOwned = Self; + + #[inline(always)] + fn html_len(&self) -> usize { + 0 + } + + fn to_html( + self, + _buf: &mut String, + _class: &mut String, + _style: &mut String, + _inner_html: &mut String, + ) { + } + + fn hydrate(self, el: &Element) -> Self::State { + self.container.load(el); + el.clone() + } + + fn build(self, el: &Element) -> Self::State { + self.container.load(el); + el.clone() + } + + fn rebuild(self, state: &mut Self::State) { + self.container.load(state); + } + + fn into_cloneable(self) -> Self::Cloneable { + self + } + + fn into_cloneable_owned(self) -> Self::CloneableOwned { + self + } + + fn dry_resolve(&mut self) {} + + async fn resolve(self) -> Self::AsyncOutput { + self + } +} + +impl NextAttribute for AnyNodeRefAttr +where + E: ElementType + 'static, + C: NodeRefContainer + Clone + 'static, + Element: PartialEq, +{ + type Output = (Self, NewAttr); + + fn add_any_attr(self, new_attr: NewAttr) -> Self::Output { + (self, new_attr) + } +} + +/// Constructs an attribute to attach an [`AnyNodeRef`] to an element. +/// +/// Enables adding node references in conditional/dynamic rendering branches. +pub fn any_node_ref(container: C) -> AnyNodeRefAttr +where + E: ElementType, + C: NodeRefContainer, +{ + AnyNodeRefAttr { + container, + ty: PhantomData, + } +} + +pub mod prelude { + pub use super::*; + pub use any_node_ref; + pub use AnyNodeRef; + pub use IntoAnyNodeRef; +} + +#[cfg(test)] +mod tests { + use leptos::{html, prelude::*}; + + use super::{any_node_ref, prelude::*}; + + #[test] + fn test_any_node_ref_creation() { + let node_ref = AnyNodeRef::new(); + assert!(node_ref.get().is_none(), "New AnyNodeRef should be empty"); + } + + #[test] + fn test_to_any_node_ref() { + let div_ref: NodeRef = NodeRef::new(); + let any_ref = div_ref.into_any(); + assert!( + any_ref.get().is_none(), + "Converted AnyNodeRef should be initially empty" + ); + } + + #[test] + fn test_clone_and_copy() { + let node_ref = AnyNodeRef::new(); + let cloned_ref = node_ref; + let _copied_ref = cloned_ref; // Should be copyable + assert!( + cloned_ref.get().is_none(), + "Cloned AnyNodeRef should be empty" + ); + } + + #[test] + fn test_default() { + let node_ref = AnyNodeRef::default(); + assert!( + node_ref.get().is_none(), + "Default AnyNodeRef should be empty" + ); + } + + #[test] + fn test_into_any_node_ref_trait() { + let div_ref: NodeRef = NodeRef::new(); + let _any_ref: AnyNodeRef = div_ref.into_any(); + + let input_ref: NodeRef = NodeRef::new(); + let _any_input_ref: AnyNodeRef = input_ref.into_any(); + } + + #[test] + fn test_from_node_ref() { + let div_ref: NodeRef = NodeRef::new(); + let _any_ref: AnyNodeRef = div_ref.into(); + } + + #[test] + fn test_any_node_ref_attr() { + let node_ref = AnyNodeRef::new(); + let _attr = any_node_ref::(node_ref); + } + + #[test] + fn test_defined_at() { + let node_ref = AnyNodeRef::new(); + assert!(node_ref.defined_at().is_some()); + } + + #[test] + fn test_track_and_untracked() { + let node_ref = AnyNodeRef::new(); + // Just testing that these don't panic + node_ref.track(); + let _untracked = node_ref.try_read_untracked(); + } + + #[test] + fn test_into_any_identity() { + let node_ref = AnyNodeRef::new(); + let same_ref = node_ref.into_any(); + + // Instead of checking pointer equality, we should verify: + // 1. Both refs are initially empty + assert!(node_ref.get().is_none()); + assert!(same_ref.get().is_none()); + + // 2. When we set one, both should reflect the change + // (This would require a mock Element to test properly) + + // 3. They should have the same defined_at location + assert_eq!(node_ref.defined_at(), same_ref.defined_at()); + } +}