Skip to main content

euv_core/vdom/cast/
impl.rs

1use super::*;
2
3/// Converts a `Vec<VirtualNode>` into a `VirtualNode::Fragment`.
4///
5/// This enables using a `Vec<VirtualNode>` directly in the `html!` macro
6/// without manually wrapping it in `VirtualNode::Fragment(...)`.
7///
8/// # Returns
9///
10/// - `VirtualNode` - A `VirtualNode::Fragment` containing the nodes, or
11///   `VirtualNode::Empty` if the vector is empty.
12impl From<Vec<VirtualNode>> for VirtualNode {
13    /// Lifts a `Vec<VirtualNode>` into a single [`VirtualNode`] via wrapping or unwrapping.
14    ///
15    /// # Arguments
16    ///
17    /// - `Vec<VirtualNode>` - Input value to convert from.
18    fn from(nodes: Vec<VirtualNode>) -> Self {
19        if nodes.is_empty() {
20            VirtualNode::Empty
21        } else {
22            VirtualNode::Fragment(nodes)
23        }
24    }
25}
26
27/// Converts a borrowed `[VirtualNode]` slice into a `VirtualNode`.
28///
29/// OPT 21: this lets call sites that hold `node.get_children()` (a
30/// `&[VirtualNode]`) hand the children straight to the `html!` macro's
31/// `(expr).into()` dispatch without first cloning into a `Vec`. The
32/// slice is materialised exactly once at the conversion site — same
33/// number of clones as the old `get_child_node` helper performed
34/// (zero for empty, one for single child, N for fragment) but moved
35/// out of the helper so any caller that just wants to peek at the
36/// children pays nothing.
37impl From<&[VirtualNode]> for VirtualNode {
38    /// Lifts a borrowed slice into a [`VirtualNode`].
39    ///
40    /// # Arguments
41    ///
42    /// - `&[VirtualNode]` - Input value to convert from.
43    fn from(children: &[VirtualNode]) -> Self {
44        match children {
45            [] => VirtualNode::Empty,
46            [only] => only.clone(),
47            _ => VirtualNode::Fragment(children.to_vec()),
48        }
49    }
50}
51
52/// Converts an `Option<VirtualNode>` into a `VirtualNode`.
53///
54/// `Some(node)` returns the inner node, `None` returns `VirtualNode::Empty`.
55///
56/// # Returns
57///
58/// - `VirtualNode` - The inner node if `Some`, otherwise `VirtualNode::Empty`.
59impl From<Option<VirtualNode>> for VirtualNode {
60    /// Lifts an `Option<VirtualNode>` into a [`VirtualNode`] (unwraps or falls back to empty).
61    ///
62    /// # Arguments
63    ///
64    /// - `Option<VirtualNode>` - Input value to convert from.
65    fn from(node: Option<VirtualNode>) -> Self {
66        match node {
67            Some(node) => node,
68            None => VirtualNode::Empty,
69        }
70    }
71}
72
73/// Converts an `Option<Vec<VirtualNode>>` into a `VirtualNode`.
74///
75/// `Some(vec)` converts the vector into a `VirtualNode::Fragment` (or `Empty`
76/// if the vector is empty), `None` returns `VirtualNode::Empty`.
77///
78/// # Returns
79///
80/// - `VirtualNode` - A `VirtualNode::Fragment` if `Some` with nodes,
81///   `VirtualNode::Empty` if `None` or the vector is empty.
82impl From<Option<Vec<VirtualNode>>> for VirtualNode {
83    /// Lifts an `Option<Vec<VirtualNode>>` into a [`VirtualNode`] (unwraps or falls back to empty).
84    ///
85    /// # Arguments
86    ///
87    /// - `Option<Vec<VirtualNode>>` - Input value to convert from.
88    fn from(nodes: Option<Vec<VirtualNode>>) -> Self {
89        match nodes {
90            Some(nodes) => nodes.into(),
91            None => VirtualNode::Empty,
92        }
93    }
94}
95
96/// Wraps a `FnMut(&mut HookContext) -> VirtualNode` closure into a `DynamicNode`.
97///
98/// This enables writing `{move |_: &mut HookContext| html! { ... }}` directly in HTML markup
99/// without explicit `DynamicNode` construction.
100impl<F> From<F> for VirtualNode
101where
102    F: FnMut(&mut HookContext) -> VirtualNode + 'static,
103{
104    /// Wraps this closure into a `VirtualNode::Dynamic` with a fresh hook context.
105    ///
106    /// # Returns
107    ///
108    /// - `VirtualNode` - A dynamic virtual node wrapping this closure.
109    ///
110    /// # Arguments
111    ///
112    /// - `F` - Input value to convert from.
113    fn from(render_fn: F) -> Self {
114        VirtualNode::create_dynamic(render_fn)
115    }
116}
117
118/// Converts a `String` into a text virtual node.
119impl From<String> for VirtualNode {
120    /// Converts this string into a text virtual node.
121    ///
122    /// # Returns
123    ///
124    /// - `VirtualNode` - A text virtual node.
125    ///
126    /// # Arguments
127    ///
128    /// - `String` - Input value to convert from.
129    fn from(text: String) -> Self {
130        VirtualNode::Text(TextNode::new(Cow::Owned(text), None))
131    }
132}
133
134/// Converts a `&str` into a text virtual node.
135impl From<&str> for VirtualNode {
136    /// Converts this string slice into a text virtual node.
137    ///
138    /// The `&str` may not be `'static`, so we route through `Cow::Owned`
139    /// (small heap allocation) — runtime-evaluated text typically comes
140    /// here via `format!`, interpolated messages, or formatted signal
141    /// values. Macro-generated literal text takes the `Cow::Borrowed`
142    /// fast path in [`crate::html::HtmlNode::Text`] instead.
143    ///
144    /// # Returns
145    ///
146    /// - `VirtualNode` - A text virtual node.
147    ///
148    /// # Arguments
149    ///
150    /// - `&str` - Input value to convert from.
151    fn from(text: &str) -> Self {
152        VirtualNode::Text(TextNode::new(Cow::Owned(text.to_owned()), None))
153    }
154}
155
156/// Converts an `i32` into a text virtual node.
157impl From<i32> for VirtualNode {
158    /// Converts this integer into a text virtual node.
159    ///
160    /// OPT 29: `i32::to_string()` is unavoidable, so the Cow wraps an
161    /// `Owned` variant. (`Cow::Owned(text)` is implicit via the
162    /// `String: Into<Cow<'static, str>>` bound, but spelled out for
163    /// consistency.)
164    ///
165    /// # Returns
166    ///
167    /// - `VirtualNode` - A text virtual node.
168    ///
169    /// # Arguments
170    ///
171    /// - `i32` - Input value to convert from.
172    fn from(value: i32) -> Self {
173        VirtualNode::Text(TextNode::new(Cow::Owned(value.to_string()), None))
174    }
175}
176
177/// Converts a `usize` into a text virtual node.
178impl From<usize> for VirtualNode {
179    /// Converts this unsigned integer into a text virtual node.
180    ///
181    /// # Returns
182    ///
183    /// - `VirtualNode` - A text virtual node.
184    ///
185    /// # Arguments
186    ///
187    /// - `usize` - Input value to convert from.
188    fn from(value: usize) -> Self {
189        VirtualNode::Text(TextNode::new(Cow::Owned(value.to_string()), None))
190    }
191}
192
193/// Converts a `bool` into a text virtual node.
194impl From<bool> for VirtualNode {
195    /// Converts this boolean into a text virtual node.
196    ///
197    /// # Returns
198    ///
199    /// - `VirtualNode` - A text virtual node.
200    ///
201    /// # Arguments
202    ///
203    /// - `bool` - Input value to convert from.
204    fn from(value: bool) -> Self {
205        VirtualNode::Text(TextNode::new(Cow::Owned(value.to_string()), None))
206    }
207}
208
209/// Converts a signal into a reactive text virtual node.
210impl<T> From<Signal<T>> for VirtualNode
211where
212    T: Clone + PartialEq + Display + 'static,
213{
214    /// Converts this signal into a reactive text virtual node.
215    ///
216    /// # Returns
217    ///
218    /// - `VirtualNode` - A reactive text virtual node.
219    ///
220    /// # Arguments
221    ///
222    /// - `Signal<T>` - Input value to convert from.
223    fn from(signal: Signal<T>) -> Self {
224        signal.as_reactive_text()
225    }
226}
227
228/// Converts a signal into a reactive text node with listener wiring.
229impl<T> AsReactiveText for Signal<T>
230where
231    T: Clone + PartialEq + Display + 'static,
232{
233    /// Creates a reactive text node that auto-updates when the signal changes.
234    ///
235    /// Internally creates a bridge `Signal<String>` that subscribes to the
236    /// source signal and updates the text content on every change.
237    ///
238    /// # Returns
239    ///
240    /// - `VirtualNode` - A text virtual node with reactive signal binding.
241    fn as_reactive_text(&self) -> VirtualNode {
242        let source: Signal<T> = *self;
243        let string_signal: Signal<String> = Signal::create(source.get().to_string());
244        let string_signal_clone: Signal<String> = string_signal;
245        source.subscribe(move || {
246            string_signal_clone.set(source.get().to_string());
247        });
248        // The closure above captures `string_signal_clone` (which aliases
249        // `string_signal`), so `source` now transitively keeps the bridge
250        // alive. Register that dependency so the bridge's heap allocation
251        // can be reclaimed once `source` is deactivated.
252        BridgeRefsCell::track(string_signal.get_inner(), source.get_inner());
253        VirtualNode::Text(TextNode::new(
254            Cow::Owned(string_signal.get()),
255            Some(string_signal),
256        ))
257    }
258}
259
260/// Constructs an `EventAdapter` that wraps any event-compatible value.
261impl<T> EventAdapter<T> {
262    /// Returns the inner wrapped value, consuming the adapter.
263    ///
264    /// # Returns
265    ///
266    /// - `T` - The inner value.
267    pub(crate) fn into_inner(self) -> T {
268        self.inner
269    }
270}
271
272/// Adapts a `FnMut(Event)` closure into an `AttributeValue::Event`.
273///
274/// Wraps the closure into a `NativeEventHandler` and returns it as an
275/// event attribute value. This replaces the `__EventWrapper<F>` type
276/// that was previously generated inline by the `html!` macro.
277impl<F> EventAdapter<F>
278where
279    F: FnMut(Event) + 'static,
280{
281    /// Converts the wrapped closure into an event `AttributeValue`.
282    ///
283    /// # Arguments
284    ///
285    /// - `&'static str` - The event name string to associate with the handler.
286    ///
287    /// # Returns
288    ///
289    /// - `AttributeValue` - An `AttributeValue::Event` wrapping the handler.
290    pub fn into_attribute(self, event_name: &'static str) -> AttributeValue {
291        AttributeValue::Event(NativeEventHandler::create(event_name, self.into_inner()))
292    }
293}
294
295/// Converts an event with a specific event name into an `AttributeValue`.
296impl<F> From<EventNamedAdapter<F>> for AttributeValue
297where
298    F: FnMut(Event) + 'static,
299{
300    /// Converts the wrapped closure with event name into an event `AttributeValue`.
301    ///
302    /// # Returns
303    ///
304    /// - `AttributeValue` - An `AttributeValue::Event` wrapping the handler.
305    ///
306    /// # Arguments
307    ///
308    /// - `EventNamedAdapter<F>` - Input value to convert from.
309    fn from(adapter: EventNamedAdapter<F>) -> Self {
310        AttributeValue::Event(NativeEventHandler::create(
311            adapter.get_event_name(),
312            adapter.inner,
313        ))
314    }
315}
316
317/// Converts an event named adapter with `NativeEventHandler` into an `AttributeValue`.
318impl From<EventNamedAdapter<NativeEventHandler>> for AttributeValue {
319    /// Converts the wrapped handler with event name into an event `AttributeValue`.
320    ///
321    /// # Returns
322    ///
323    /// - `AttributeValue` - An `AttributeValue::Event` wrapping the handler.
324    ///
325    /// # Arguments
326    ///
327    /// - `EventNamedAdapter<NativeEventHandler>` - Input value to convert from.
328    fn from(mut adapter: EventNamedAdapter<NativeEventHandler>) -> Self {
329        let event_name: &'static str = adapter.get_event_name();
330        adapter.get_mut_inner().set_event_name(event_name);
331        AttributeValue::Event(adapter.inner)
332    }
333}
334
335/// Converts an event named adapter with optional shared closure into an `AttributeValue`.
336///
337/// `Some(callback)` becomes `AttributeValue::Event` by wrapping the shared closure
338/// into a `NativeEventHandler` with the adapter's event name, and `None` becomes
339/// `AttributeValue::Text(String::new())`.
340impl From<EventNamedAdapter<Option<Rc<dyn Fn(Event)>>>> for AttributeValue {
341    /// Converts the wrapped optional shared closure with event name into an event `AttributeValue`.
342    ///
343    /// # Returns
344    ///
345    /// - `AttributeValue` - An event attribute if `Some`, otherwise an empty text attribute.
346    ///
347    /// # Arguments
348    ///
349    /// - `EventNamedAdapter<Option<Rc<dyn Fn(Event)>>>` - Input value to convert from.
350    fn from(adapter: EventNamedAdapter<Option<Rc<dyn Fn(Event)>>>) -> Self {
351        let event_name: &'static str = adapter.get_event_name();
352        match adapter.inner {
353            Some(callback) => AttributeValue::Event(NativeEventHandler::create(
354                event_name,
355                move |event: Event| {
356                    callback(event);
357                },
358            )),
359            None => AttributeValue::Text(String::new()),
360        }
361    }
362}
363
364/// Adapts an owned `NativeEventHandler` into an `AttributeValue::Event` directly.
365///
366/// When the user already provides a `NativeEventHandler`, the handler is
367/// re-wrapped with the given `event_name` to ensure the DOM event listener
368/// is bound to the correct event type (e.g., "click" rather than "onclick").
369impl EventAdapter<NativeEventHandler> {
370    /// Converts the wrapped handler into an event `AttributeValue`.
371    ///
372    /// Re-wraps the handler with the provided `event_name` so that the
373    /// DOM event listener uses the correct event type string.
374    ///
375    /// # Arguments
376    ///
377    /// - `&'static str` - The event name to bind the handler to.
378    ///
379    /// # Returns
380    ///
381    /// - `AttributeValue` - An `AttributeValue::Event` containing the re-wrapped handler.
382    pub fn into_attribute(self, event_name: &'static str) -> AttributeValue {
383        let mut handler: NativeEventHandler = self.into_inner();
384        handler.set_event_name(event_name);
385        AttributeValue::Event(handler)
386    }
387}
388
389/// Adapts an `Option<NativeEventHandler>` into an `AttributeValue`.
390///
391/// `Some(handler)` becomes `AttributeValue::Event(handler)` re-wrapped with the
392/// given event name, and `None` becomes `AttributeValue::Text(String::new())`.
393impl EventAdapter<Option<NativeEventHandler>> {
394    /// Converts the wrapped optional handler into an attribute value.
395    ///
396    /// Re-wraps a `Some` handler with the provided `event_name` so that the
397    /// DOM event listener uses the correct event type string.
398    ///
399    /// # Arguments
400    ///
401    /// - `&'static str` - The event name to bind the handler to.
402    ///
403    /// # Returns
404    ///
405    /// - `AttributeValue` - An event attribute if `Some`, otherwise an empty text attribute.
406    pub fn into_attribute(self, event_name: &'static str) -> AttributeValue {
407        match self.into_inner() {
408            Some(handler) => EventNamedAdapter::new(handler, event_name).into(),
409            None => AttributeValue::Text(String::new()),
410        }
411    }
412}
413
414/// Adapts an `Option<Rc<dyn Fn(Event)>>` into an `AttributeValue`.
415///
416/// `Some(callback)` becomes `AttributeValue::Event` by wrapping the shared closure
417/// into a `NativeEventHandler`, and `None` becomes `AttributeValue::Text(String::new())`.
418/// This supports component Props that use `Option<Rc<dyn Fn(Event)>>` for event callbacks.
419impl EventAdapter<Option<Rc<dyn Fn(Event)>>> {
420    /// Converts the wrapped optional shared closure into an attribute value.
421    ///
422    /// # Arguments
423    ///
424    /// - `&'static str` - The event name to bind the handler to.
425    ///
426    /// # Returns
427    ///
428    /// - `AttributeValue` - An event attribute if `Some`, otherwise an empty text attribute.
429    pub fn into_attribute(self, event_name: &'static str) -> AttributeValue {
430        match self.into_inner() {
431            Some(callback) => AttributeValue::Event(NativeEventHandler::create(
432                event_name,
433                move |event: Event| {
434                    callback(event);
435                },
436            )),
437            None => AttributeValue::Text(String::new()),
438        }
439    }
440}
441
442/// Constructs an `AttrValueAdapter` that wraps any attribute-compatible value.
443impl<T> AttrValueAdapter<T> {
444    /// Returns the inner wrapped value, consuming the adapter.
445    ///
446    /// # Returns
447    ///
448    /// - `T` - The inner value.
449    pub(crate) fn into_inner(self) -> T {
450        self.inner
451    }
452}
453
454/// Constructs an `InnerHtmlAdapter` that wraps an `inner_html:` payload.
455impl<T> InnerHtmlAdapter<T> {
456    /// Returns the inner wrapped value, consuming the adapter.
457    ///
458    /// Mirrors [`AttrValueAdapter::into_inner`] so the html! macro can
459    /// use the same "wrap-then-into-inner" pattern for both adapter
460    /// kinds without diverging call sites.
461    ///
462    /// # Returns
463    ///
464    /// - `T` - The inner value.
465    pub(crate) fn into_inner(self) -> T {
466        self.inner
467    }
468}
469
470/// Adapts a `FnMut(Event)` closure into a callback `AttributeValue`.
471///
472/// This handles the case where a closure is used as a component callback prop.
473/// The closure is converted via `IntoCallbackAttribute::into_callback_attribute()`.
474impl<F> AttrValueAdapter<F>
475where
476    F: FnMut(Event) + 'static,
477{
478    /// Converts the wrapped closure into a callback `AttributeValue`.
479    ///
480    /// # Returns
481    ///
482    /// - `AttributeValue` - An event attribute value wrapping the adapted closure.
483    pub fn into_callback(self) -> AttributeValue {
484        self.into_inner().into()
485    }
486
487    /// Converts the wrapped closure into a callback `AttributeValue` with a
488    /// custom event name for component props.
489    ///
490    /// # Arguments
491    ///
492    /// - `&'static str` - The custom attribute name (e.g., "on-increment", "on-change").
493    ///
494    /// # Returns
495    ///
496    /// - `AttributeValue` - An event attribute value with the custom name.
497    pub fn into_callback_named(self, name: &'static str) -> AttributeValue {
498        AttributeValue::Event(NativeEventHandler::create(name, self.into_inner()))
499    }
500}
501
502/// Converts a named callback adapter into an `AttributeValue`.
503impl<F> From<CallbackNamedAdapter<F>> for AttributeValue
504where
505    F: FnMut(Event) + 'static,
506{
507    /// Converts the wrapped closure with custom name into a callback `AttributeValue`.
508    ///
509    /// # Returns
510    ///
511    /// - `AttributeValue` - An event attribute value with the custom name.
512    ///
513    /// # Arguments
514    ///
515    /// - `CallbackNamedAdapter<F>` - Input value to convert from.
516    fn from(adapter: CallbackNamedAdapter<F>) -> Self {
517        AttributeValue::Event(NativeEventHandler::create(
518            adapter.get_name(),
519            adapter.inner,
520        ))
521    }
522}
523
524/// Generic-parameterised implementation for [`AttrValueAdapter`].
525/// Implements `impl AttrValueAdapter<NativeEventHandler>`.
526impl AttrValueAdapter<NativeEventHandler> {
527    /// Converts the wrapped handler into a callback `AttributeValue` with a
528    /// custom event name for component props.
529    ///
530    /// # Arguments
531    ///
532    /// - `&'static str` - The custom attribute name.
533    ///
534    /// # Returns
535    ///
536    /// - `AttributeValue` - An event attribute value with the custom name.
537    pub fn into_callback_named(self, name: &'static str) -> AttributeValue {
538        let mut handler: NativeEventHandler = self.into_inner();
539        handler.set_event_name(name);
540        AttributeValue::Event(handler)
541    }
542}
543
544/// Adapts an `Option<NativeEventHandler>` into an `AttributeValue`.
545impl AttrValueAdapter<Option<NativeEventHandler>> {
546    /// Converts the wrapped optional handler into an attribute value.
547    ///
548    /// # Returns
549    ///
550    /// - `AttributeValue` - An event attribute if `Some`, otherwise an empty text attribute.
551    pub fn into_callback(self) -> AttributeValue {
552        match self.into_inner() {
553            Some(handler) => AttrValueAdapter::new(handler).into(),
554            None => AttributeValue::Text(String::new()),
555        }
556    }
557
558    /// Converts this optional handler into a callback `AttributeValue` with a
559    /// custom event name for component props.
560    ///
561    /// # Arguments
562    ///
563    /// - `&'static str` - The custom attribute name.
564    ///
565    /// # Returns
566    ///
567    /// - `AttributeValue` - An event attribute with the custom name if `Some`,
568    ///   otherwise an empty text attribute.
569    pub fn into_callback_named(self, name: &'static str) -> AttributeValue {
570        match self.into_inner() {
571            Some(handler) => AttrValueAdapter::new(handler).into_callback_named(name),
572            None => AttributeValue::Text(String::new()),
573        }
574    }
575}
576
577/// Adapts any type that implements `Into<AttributeValue>` into an `AttributeValue`.
578///
579/// This is the fallback path for non-closure attribute values (strings, signals,
580/// CSS classes, etc.).
581impl<T> From<AttrValueAdapter<T>> for AttributeValue
582where
583    T: Into<AttributeValue>,
584{
585    /// Converts the wrapped value into an `AttributeValue`.
586    ///
587    /// # Returns
588    ///
589    /// - `AttributeValue` - The reactive attribute value.
590    ///
591    /// # Arguments
592    ///
593    /// - `AttrValueAdapter<T>` - Input value to convert from.
594    fn from(adapter: AttrValueAdapter<T>) -> Self {
595        adapter.into_inner().into()
596    }
597}
598
599/// Adapts an `inner_html:` payload into the matching `AttributeValue`
600/// variant, routing through `set_inner_html` instead of the generic
601/// `Text` attribute path.
602///
603/// The two blanket impls below cover the user-visible call sites:
604///
605/// - `String` produces
606///   `AttributeValue::InnerHtml(String::from("..."))`.
607/// - `Signal<String>` produces
608///   `AttributeValue::InnerHtmlSignal(signal)`.
609///
610/// Each impl only requires its specific source type, so the compiler
611/// picks the right one based on the inferred `T` at the call site
612/// (no manual `.into()` annotation needed).
613impl From<InnerHtmlAdapter<String>> for AttributeValue {
614    /// Wraps the static `String` payload in an
615    /// `AttributeValue::InnerHtml` variant so the renderer can call
616    /// `Element::set_inner_html` on it.
617    ///
618    /// # Arguments
619    ///
620    /// - `InnerHtmlAdapter<String>` - Input value to convert from.
621    fn from(adapter: InnerHtmlAdapter<String>) -> Self {
622        AttributeValue::InnerHtml(adapter.into_inner())
623    }
624}
625
626/// `From` conversion into [`AttributeValue`].
627impl From<InnerHtmlAdapter<&str>> for AttributeValue {
628    /// Wraps the static `&str` payload by allocating a new `String`
629    /// so the renderer owns the data independently of the caller's
630    /// borrow lifetime.
631    ///
632    /// # Arguments
633    ///
634    /// - `InnerHtmlAdapter<&str>` - Input value to convert from.
635    fn from(adapter: InnerHtmlAdapter<&str>) -> Self {
636        AttributeValue::InnerHtml(adapter.into_inner().to_owned())
637    }
638}
639
640/// `From` conversion into [`AttributeValue`].
641impl From<InnerHtmlAdapter<Signal<String>>> for AttributeValue {
642    /// Wraps the reactive payload in an `AttributeValue::InnerHtmlSignal`
643    /// so the renderer subscribes to the signal and re-applies
644    /// `set_inner_html` on every change.
645    ///
646    /// # Arguments
647    ///
648    /// - `InnerHtmlAdapter<Signal<String>>` - Input value to convert from.
649    fn from(adapter: InnerHtmlAdapter<Signal<String>>) -> Self {
650        AttributeValue::InnerHtmlSignal(adapter.into_inner())
651    }
652}
653
654/// Converts a `NodeRef<T>` into an `AttributeValue::Ref`.
655///
656/// The type parameter `T` is erased at the `AttributeValue` layer (we
657/// store the underlying `JsValue` cell), so any concrete element type
658/// works. The `html!` macro relies on this to accept
659/// `html! { input { ref: my_ref } }` without the user needing to call
660/// `.into()` explicitly.
661impl<T: ?Sized> From<NodeRef<T>> for AttributeValue {
662    /// Wraps the ref cell as an `AttributeValue::Ref` so the renderer
663    /// can populate it after mount.
664    ///
665    /// # Returns
666    ///
667    /// - `AttributeValue` - A ref variant carrying the (still empty)
668    ///   handle.
669    ///
670    /// # Arguments
671    ///
672    /// - `NodeRef<T>` - Input value to convert from.
673    fn from(node_ref: NodeRef<T>) -> Self {
674        // Erase the typed phantom marker to a `NodeRef<JsValue>` so the
675        // `AttributeValue::Ref` payload is uniform. The `JsValue` cell
676        // is what the renderer actually stores and sets.
677        let erased: NodeRefDyn = NodeRefDyn {
678            inner: node_ref.inner.clone(),
679            _marker: PhantomData,
680        };
681        AttributeValue::Ref(erased)
682    }
683}