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    /// The returned `TextNode` carries a binder closure instead of an
236    /// intermediate bridge signal. The binder runs exactly once per DOM text
237    /// node at materialization time: it subscribes the source signal directly
238    /// to that node (converting `T` to `String` on every change), guarded by
239    /// `is_connected` so writes stop once the node leaves the document. No
240    /// per-render bridge allocation and no subscription churn: a kept text
241    /// node keeps its single subscription across any number of re-renders of
242    /// its parent.
243    ///
244    /// # Returns
245    ///
246    /// - `VirtualNode` - A text virtual node with reactive signal binding.
247    fn as_reactive_text(&self) -> VirtualNode {
248        let source: Signal<T> = *self;
249        let binder: Rc<dyn Fn(&Text)> = Rc::new(move |text: &Text| {
250            let text_node: Text = text.clone();
251            source.subscribe(move || {
252                if !text_node.is_connected() {
253                    return;
254                }
255                let value: String = source.get().to_string();
256                text_node.set_text_content(Some(&value));
257            });
258        });
259        VirtualNode::Text(TextNode::new(
260            Cow::Owned(source.get().to_string()),
261            Some(binder),
262        ))
263    }
264}
265
266/// Constructs an `EventAdapter` that wraps any event-compatible value.
267impl<T> EventAdapter<T> {
268    /// Returns the inner wrapped value, consuming the adapter.
269    ///
270    /// # Returns
271    ///
272    /// - `T` - The inner value.
273    pub(crate) fn into_inner(self) -> T {
274        self.inner
275    }
276}
277
278/// Adapts a `FnMut(Event)` closure into an `AttributeValue::Event`.
279///
280/// Wraps the closure into a `NativeEventHandler` and returns it as an
281/// event attribute value. This replaces the `__EventWrapper<F>` type
282/// that was previously generated inline by the `html!` macro.
283impl<F> EventAdapter<F>
284where
285    F: FnMut(Event) + 'static,
286{
287    /// Converts the wrapped closure into an event `AttributeValue`.
288    ///
289    /// # Arguments
290    ///
291    /// - `&'static str` - The event name string to associate with the handler.
292    ///
293    /// # Returns
294    ///
295    /// - `AttributeValue` - An `AttributeValue::Event` wrapping the handler.
296    pub fn into_attribute(self, event_name: &'static str) -> AttributeValue {
297        AttributeValue::Event(NativeEventHandler::create(event_name, self.into_inner()))
298    }
299}
300
301/// Converts an event with a specific event name into an `AttributeValue`.
302impl<F> From<EventNamedAdapter<F>> for AttributeValue
303where
304    F: FnMut(Event) + 'static,
305{
306    /// Converts the wrapped closure with event name into an event `AttributeValue`.
307    ///
308    /// # Returns
309    ///
310    /// - `AttributeValue` - An `AttributeValue::Event` wrapping the handler.
311    ///
312    /// # Arguments
313    ///
314    /// - `EventNamedAdapter<F>` - Input value to convert from.
315    fn from(adapter: EventNamedAdapter<F>) -> Self {
316        AttributeValue::Event(NativeEventHandler::create(
317            adapter.get_event_name(),
318            adapter.inner,
319        ))
320    }
321}
322
323/// Converts an event named adapter with `NativeEventHandler` into an `AttributeValue`.
324impl From<EventNamedAdapter<NativeEventHandler>> for AttributeValue {
325    /// Converts the wrapped handler with event name into an event `AttributeValue`.
326    ///
327    /// # Returns
328    ///
329    /// - `AttributeValue` - An `AttributeValue::Event` wrapping the handler.
330    ///
331    /// # Arguments
332    ///
333    /// - `EventNamedAdapter<NativeEventHandler>` - Input value to convert from.
334    fn from(mut adapter: EventNamedAdapter<NativeEventHandler>) -> Self {
335        let event_name: &'static str = adapter.get_event_name();
336        adapter.get_mut_inner().set_event_name(event_name);
337        AttributeValue::Event(adapter.inner)
338    }
339}
340
341/// Converts an event named adapter with optional shared closure into an `AttributeValue`.
342///
343/// `Some(callback)` becomes `AttributeValue::Event` by wrapping the shared closure
344/// into a `NativeEventHandler` with the adapter's event name, and `None` becomes
345/// `AttributeValue::Text(String::new())`.
346impl From<EventNamedAdapter<Option<Rc<dyn Fn(Event)>>>> for AttributeValue {
347    /// Converts the wrapped optional shared closure with event name into an event `AttributeValue`.
348    ///
349    /// # Returns
350    ///
351    /// - `AttributeValue` - An event attribute if `Some`, otherwise an empty text attribute.
352    ///
353    /// # Arguments
354    ///
355    /// - `EventNamedAdapter<Option<Rc<dyn Fn(Event)>>>` - Input value to convert from.
356    fn from(adapter: EventNamedAdapter<Option<Rc<dyn Fn(Event)>>>) -> Self {
357        let event_name: &'static str = adapter.get_event_name();
358        match adapter.inner {
359            Some(callback) => AttributeValue::Event(NativeEventHandler::create(
360                event_name,
361                move |event: Event| {
362                    callback(event);
363                },
364            )),
365            None => AttributeValue::Text(String::new()),
366        }
367    }
368}
369
370/// Adapts an owned `NativeEventHandler` into an `AttributeValue::Event` directly.
371///
372/// When the user already provides a `NativeEventHandler`, the handler is
373/// re-wrapped with the given `event_name` to ensure the DOM event listener
374/// is bound to the correct event type (e.g., "click" rather than "onclick").
375impl EventAdapter<NativeEventHandler> {
376    /// Converts the wrapped handler into an event `AttributeValue`.
377    ///
378    /// Re-wraps the handler with the provided `event_name` so that the
379    /// DOM event listener uses the correct event type string.
380    ///
381    /// # Arguments
382    ///
383    /// - `&'static str` - The event name to bind the handler to.
384    ///
385    /// # Returns
386    ///
387    /// - `AttributeValue` - An `AttributeValue::Event` containing the re-wrapped handler.
388    pub fn into_attribute(self, event_name: &'static str) -> AttributeValue {
389        let mut handler: NativeEventHandler = self.into_inner();
390        handler.set_event_name(event_name);
391        AttributeValue::Event(handler)
392    }
393}
394
395/// Adapts an `Option<NativeEventHandler>` into an `AttributeValue`.
396///
397/// `Some(handler)` becomes `AttributeValue::Event(handler)` re-wrapped with the
398/// given event name, and `None` becomes `AttributeValue::Text(String::new())`.
399impl EventAdapter<Option<NativeEventHandler>> {
400    /// Converts the wrapped optional handler into an attribute value.
401    ///
402    /// Re-wraps a `Some` handler with the provided `event_name` so that the
403    /// DOM event listener uses the correct event type string.
404    ///
405    /// # Arguments
406    ///
407    /// - `&'static str` - The event name to bind the handler to.
408    ///
409    /// # Returns
410    ///
411    /// - `AttributeValue` - An event attribute if `Some`, otherwise an empty text attribute.
412    pub fn into_attribute(self, event_name: &'static str) -> AttributeValue {
413        match self.into_inner() {
414            Some(handler) => EventNamedAdapter::new(handler, event_name).into(),
415            None => AttributeValue::Text(String::new()),
416        }
417    }
418}
419
420/// Adapts an `Option<Rc<dyn Fn(Event)>>` into an `AttributeValue`.
421///
422/// `Some(callback)` becomes `AttributeValue::Event` by wrapping the shared closure
423/// into a `NativeEventHandler`, and `None` becomes `AttributeValue::Text(String::new())`.
424/// This supports component Props that use `Option<Rc<dyn Fn(Event)>>` for event callbacks.
425impl EventAdapter<Option<Rc<dyn Fn(Event)>>> {
426    /// Converts the wrapped optional shared closure into an attribute value.
427    ///
428    /// # Arguments
429    ///
430    /// - `&'static str` - The event name to bind the handler to.
431    ///
432    /// # Returns
433    ///
434    /// - `AttributeValue` - An event attribute if `Some`, otherwise an empty text attribute.
435    pub fn into_attribute(self, event_name: &'static str) -> AttributeValue {
436        match self.into_inner() {
437            Some(callback) => AttributeValue::Event(NativeEventHandler::create(
438                event_name,
439                move |event: Event| {
440                    callback(event);
441                },
442            )),
443            None => AttributeValue::Text(String::new()),
444        }
445    }
446}
447
448/// Constructs an `AttrValueAdapter` that wraps any attribute-compatible value.
449impl<T> AttrValueAdapter<T> {
450    /// Returns the inner wrapped value, consuming the adapter.
451    ///
452    /// # Returns
453    ///
454    /// - `T` - The inner value.
455    pub(crate) fn into_inner(self) -> T {
456        self.inner
457    }
458}
459
460/// Constructs an `InnerHtmlAdapter` that wraps an `inner_html:` payload.
461impl<T> InnerHtmlAdapter<T> {
462    /// Returns the inner wrapped value, consuming the adapter.
463    ///
464    /// Mirrors [`AttrValueAdapter::into_inner`] so the html! macro can
465    /// use the same "wrap-then-into-inner" pattern for both adapter
466    /// kinds without diverging call sites.
467    ///
468    /// # Returns
469    ///
470    /// - `T` - The inner value.
471    pub(crate) fn into_inner(self) -> T {
472        self.inner
473    }
474}
475
476/// Adapts a `FnMut(Event)` closure into a callback `AttributeValue`.
477///
478/// This handles the case where a closure is used as a component callback prop.
479/// The closure is converted via `IntoCallbackAttribute::into_callback_attribute()`.
480impl<F> AttrValueAdapter<F>
481where
482    F: FnMut(Event) + 'static,
483{
484    /// Converts the wrapped closure into a callback `AttributeValue`.
485    ///
486    /// # Returns
487    ///
488    /// - `AttributeValue` - An event attribute value wrapping the adapted closure.
489    pub fn into_callback(self) -> AttributeValue {
490        self.into_inner().into()
491    }
492
493    /// Converts the wrapped closure into a callback `AttributeValue` with a
494    /// custom event name for component props.
495    ///
496    /// # Arguments
497    ///
498    /// - `&'static str` - The custom attribute name (e.g., "on-increment", "on-change").
499    ///
500    /// # Returns
501    ///
502    /// - `AttributeValue` - An event attribute value with the custom name.
503    pub fn into_callback_named(self, name: &'static str) -> AttributeValue {
504        AttributeValue::Event(NativeEventHandler::create(name, self.into_inner()))
505    }
506}
507
508/// Converts a named callback adapter into an `AttributeValue`.
509impl<F> From<CallbackNamedAdapter<F>> for AttributeValue
510where
511    F: FnMut(Event) + 'static,
512{
513    /// Converts the wrapped closure with custom name into a callback `AttributeValue`.
514    ///
515    /// # Returns
516    ///
517    /// - `AttributeValue` - An event attribute value with the custom name.
518    ///
519    /// # Arguments
520    ///
521    /// - `CallbackNamedAdapter<F>` - Input value to convert from.
522    fn from(adapter: CallbackNamedAdapter<F>) -> Self {
523        AttributeValue::Event(NativeEventHandler::create(
524            adapter.get_name(),
525            adapter.inner,
526        ))
527    }
528}
529
530/// Generic-parameterised implementation for [`AttrValueAdapter`].
531/// Implements `impl AttrValueAdapter<NativeEventHandler>`.
532impl AttrValueAdapter<NativeEventHandler> {
533    /// Converts the wrapped handler into a callback `AttributeValue` with a
534    /// custom event name for component props.
535    ///
536    /// # Arguments
537    ///
538    /// - `&'static str` - The custom attribute name.
539    ///
540    /// # Returns
541    ///
542    /// - `AttributeValue` - An event attribute value with the custom name.
543    pub fn into_callback_named(self, name: &'static str) -> AttributeValue {
544        let mut handler: NativeEventHandler = self.into_inner();
545        handler.set_event_name(name);
546        AttributeValue::Event(handler)
547    }
548}
549
550/// Adapts an `Option<NativeEventHandler>` into an `AttributeValue`.
551impl AttrValueAdapter<Option<NativeEventHandler>> {
552    /// Converts the wrapped optional handler into an attribute value.
553    ///
554    /// # Returns
555    ///
556    /// - `AttributeValue` - An event attribute if `Some`, otherwise an empty text attribute.
557    pub fn into_callback(self) -> AttributeValue {
558        match self.into_inner() {
559            Some(handler) => AttrValueAdapter::new(handler).into(),
560            None => AttributeValue::Text(String::new()),
561        }
562    }
563
564    /// Converts this optional handler into a callback `AttributeValue` with a
565    /// custom event name for component props.
566    ///
567    /// # Arguments
568    ///
569    /// - `&'static str` - The custom attribute name.
570    ///
571    /// # Returns
572    ///
573    /// - `AttributeValue` - An event attribute with the custom name if `Some`,
574    ///   otherwise an empty text attribute.
575    pub fn into_callback_named(self, name: &'static str) -> AttributeValue {
576        match self.into_inner() {
577            Some(handler) => AttrValueAdapter::new(handler).into_callback_named(name),
578            None => AttributeValue::Text(String::new()),
579        }
580    }
581}
582
583/// Adapts any type that implements `Into<AttributeValue>` into an `AttributeValue`.
584///
585/// This is the fallback path for non-closure attribute values (strings, signals,
586/// CSS classes, etc.).
587impl<T> From<AttrValueAdapter<T>> for AttributeValue
588where
589    T: Into<AttributeValue>,
590{
591    /// Converts the wrapped value into an `AttributeValue`.
592    ///
593    /// # Returns
594    ///
595    /// - `AttributeValue` - The reactive attribute value.
596    ///
597    /// # Arguments
598    ///
599    /// - `AttrValueAdapter<T>` - Input value to convert from.
600    fn from(adapter: AttrValueAdapter<T>) -> Self {
601        adapter.into_inner().into()
602    }
603}
604
605/// Adapts an `inner_html:` payload into the matching `AttributeValue`
606/// variant, routing through `set_inner_html` instead of the generic
607/// `Text` attribute path.
608///
609/// The two blanket impls below cover the user-visible call sites:
610///
611/// - `String` produces
612///   `AttributeValue::InnerHtml(String::from("..."))`.
613/// - `Signal<String>` produces
614///   `AttributeValue::InnerHtmlSignal(signal)`.
615///
616/// Each impl only requires its specific source type, so the compiler
617/// picks the right one based on the inferred `T` at the call site
618/// (no manual `.into()` annotation needed).
619impl From<InnerHtmlAdapter<String>> for AttributeValue {
620    /// Wraps the static `String` payload in an
621    /// `AttributeValue::InnerHtml` variant so the renderer can call
622    /// `Element::set_inner_html` on it.
623    ///
624    /// # Arguments
625    ///
626    /// - `InnerHtmlAdapter<String>` - Input value to convert from.
627    fn from(adapter: InnerHtmlAdapter<String>) -> Self {
628        AttributeValue::InnerHtml(adapter.into_inner())
629    }
630}
631
632/// `From` conversion into [`AttributeValue`].
633impl From<InnerHtmlAdapter<&str>> for AttributeValue {
634    /// Wraps the static `&str` payload by allocating a new `String`
635    /// so the renderer owns the data independently of the caller's
636    /// borrow lifetime.
637    ///
638    /// # Arguments
639    ///
640    /// - `InnerHtmlAdapter<&str>` - Input value to convert from.
641    fn from(adapter: InnerHtmlAdapter<&str>) -> Self {
642        AttributeValue::InnerHtml(adapter.into_inner().to_owned())
643    }
644}
645
646/// `From` conversion into [`AttributeValue`].
647impl From<InnerHtmlAdapter<Signal<String>>> for AttributeValue {
648    /// Wraps the reactive payload in an `AttributeValue::InnerHtmlSignal`
649    /// so the renderer subscribes to the signal and re-applies
650    /// `set_inner_html` on every change.
651    ///
652    /// # Arguments
653    ///
654    /// - `InnerHtmlAdapter<Signal<String>>` - Input value to convert from.
655    fn from(adapter: InnerHtmlAdapter<Signal<String>>) -> Self {
656        AttributeValue::InnerHtmlSignal(adapter.into_inner())
657    }
658}
659
660/// Converts a `NodeRef<T>` into an `AttributeValue::Ref`.
661///
662/// The type parameter `T` is erased at the `AttributeValue` layer (we
663/// store the underlying `JsValue` cell), so any concrete element type
664/// works. The `html!` macro relies on this to accept
665/// `html! { input { ref: my_ref } }` without the user needing to call
666/// `.into()` explicitly.
667impl<T: ?Sized> From<NodeRef<T>> for AttributeValue {
668    /// Wraps the ref cell as an `AttributeValue::Ref` so the renderer
669    /// can populate it after mount.
670    ///
671    /// # Returns
672    ///
673    /// - `AttributeValue` - A ref variant carrying the (still empty)
674    ///   handle.
675    ///
676    /// # Arguments
677    ///
678    /// - `NodeRef<T>` - Input value to convert from.
679    fn from(node_ref: NodeRef<T>) -> Self {
680        // Erase the typed phantom marker to a `NodeRef<JsValue>` so the
681        // `AttributeValue::Ref` payload is uniform. The `JsValue` cell
682        // is what the renderer actually stores and sets.
683        let erased: NodeRefDyn = NodeRefDyn {
684            inner: node_ref.inner.clone(),
685            _marker: PhantomData,
686        };
687        AttributeValue::Ref(erased)
688    }
689}