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