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