Skip to main content
This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew/virtual_dom/
vtag.rs

1//! This module contains the implementation of a virtual element node [VTag].
2
3use std::marker::PhantomData;
4use std::mem;
5use std::ops::{Deref, DerefMut};
6use std::rc::Rc;
7
8use wasm_bindgen::JsValue;
9use web_sys::{HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement};
10
11use super::{AttrValue, AttributeOrProperty, Attributes, Key, Listener, Listeners, VNode};
12use crate::html::{ImplicitClone, IntoPropValue, NodeRef};
13
14/// SVG namespace string used for creating svg elements
15pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg";
16
17/// MathML namespace string used for creating MathML elements
18pub const MATHML_NAMESPACE: &str = "http://www.w3.org/1998/Math/MathML";
19
20/// Default namespace for html elements
21pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
22
23/// Value field corresponding to an [Element]'s `value` property
24#[derive(Debug, Eq, PartialEq)]
25pub(crate) struct Value<T>(Option<AttrValue>, PhantomData<T>);
26
27impl<T> Clone for Value<T> {
28    fn clone(&self) -> Self {
29        Self::new(self.0.clone())
30    }
31}
32
33impl<T> ImplicitClone for Value<T> {}
34
35impl<T> Default for Value<T> {
36    fn default() -> Self {
37        Self::new(None)
38    }
39}
40
41impl<T> Value<T> {
42    /// Create a new value. The caller should take care that the value is valid for the element's
43    /// `value` property
44    fn new(value: Option<AttrValue>) -> Self {
45        Value(value, PhantomData)
46    }
47
48    /// Set a new value. The caller should take care that the value is valid for the element's
49    /// `value` property
50    pub(crate) fn set(&mut self, value: Option<AttrValue>) {
51        self.0 = value;
52    }
53}
54
55impl<T> Deref for Value<T> {
56    type Target = Option<AttrValue>;
57
58    fn deref(&self) -> &Self::Target {
59        &self.0
60    }
61}
62
63/// Fields specific to
64/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag](crate::virtual_dom::VTag)s
65#[derive(Debug, Clone, ImplicitClone, Default, Eq, PartialEq)]
66pub(crate) struct InputFields {
67    /// Contains a value of an
68    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
69    pub(crate) value: Value<InputElement>,
70    /// Represents `checked` attribute of
71    /// [input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-checked).
72    /// It exists to override standard behavior of `checked` attribute, because
73    /// in original HTML it sets `defaultChecked` value of `InputElement`, but for reactive
74    /// frameworks it's more useful to control `checked` value of an `InputElement`.
75    pub(crate) checked: Option<bool>,
76}
77
78impl Deref for InputFields {
79    type Target = Value<InputElement>;
80
81    fn deref(&self) -> &Self::Target {
82        &self.value
83    }
84}
85
86impl DerefMut for InputFields {
87    fn deref_mut(&mut self) -> &mut Self::Target {
88        &mut self.value
89    }
90}
91
92impl InputFields {
93    /// Create new attributes for an [InputElement] element
94    fn new(value: Option<AttrValue>, checked: Option<bool>) -> Self {
95        Self {
96            value: Value::new(value),
97            checked,
98        }
99    }
100}
101
102#[derive(Debug, Clone, Default)]
103pub(crate) struct TextareaFields {
104    /// Contains the value of an
105    /// [TextAreaElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea).
106    pub(crate) value: Value<TextAreaElement>,
107    /// Contains the default value of
108    /// [TextAreaElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea).
109    #[allow(unused)] // unused only if both "csr" and "ssr" features are off
110    pub(crate) defaultvalue: Option<AttrValue>,
111}
112
113/// [VTag] fields that are specific to different [VTag] kinds.
114/// Decreases the memory footprint of [VTag] by avoiding impossible field and value combinations.
115#[derive(Debug, Clone, ImplicitClone)]
116pub(crate) enum VTagInner {
117    /// Fields specific to
118    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)
119    /// [VTag]s
120    Input(InputFields),
121    /// Fields specific to
122    /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)
123    /// [VTag]s
124    Textarea(TextareaFields),
125    /// Fields for all other kinds of [VTag]s
126    Other {
127        /// A tag of the element.
128        tag: AttrValue,
129        /// children of the element.
130        children: VNode,
131    },
132}
133
134/// A type for a virtual
135/// [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element)
136/// representation.
137#[derive(Debug, Clone, ImplicitClone)]
138pub struct VTag {
139    /// [VTag] fields that are specific to different [VTag] kinds.
140    pub(crate) inner: VTagInner,
141    /// List of attached listeners.
142    pub(crate) listeners: Listeners,
143    /// A node reference used for DOM access in Component lifecycle methods
144    pub node_ref: NodeRef,
145    /// List of attributes.
146    pub attributes: Attributes,
147    pub key: Option<Key>,
148}
149
150impl VTag {
151    /// Creates a new [VTag] instance with `tag` name (cannot be changed later in DOM).
152    pub fn new(tag: impl Into<AttrValue>) -> Self {
153        let tag = tag.into();
154        let lowercase_tag = tag.to_ascii_lowercase();
155        Self::new_base(
156            match &*lowercase_tag {
157                "input" => VTagInner::Input(Default::default()),
158                "textarea" => VTagInner::Textarea(Default::default()),
159                _ => VTagInner::Other {
160                    tag,
161                    children: Default::default(),
162                },
163            },
164            Default::default(),
165            Default::default(),
166            Default::default(),
167            Default::default(),
168        )
169    }
170
171    /// Creates a new
172    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag]
173    /// instance.
174    ///
175    /// Unlike [VTag::new()], this sets all the public fields of [VTag] in one call. This allows the
176    /// compiler to inline property and child list construction in the `html!` macro. This enables
177    /// higher instruction parallelism by reducing data dependency and avoids `memcpy` of Vtag
178    /// fields.
179    #[doc(hidden)]
180    pub fn __new_input(
181        value: Option<AttrValue>,
182        checked: Option<bool>,
183        node_ref: NodeRef,
184        key: Option<Key>,
185        // at the bottom for more readable macro-expanded code
186        attributes: Attributes,
187        listeners: Listeners,
188    ) -> Self {
189        VTag::new_base(
190            VTagInner::Input(InputFields::new(
191                value,
192                // In HTML node `checked` attribute sets `defaultChecked` parameter,
193                // but we use own field to control real `checked` parameter
194                checked,
195            )),
196            node_ref,
197            key,
198            attributes,
199            listeners,
200        )
201    }
202
203    /// Creates a new
204    /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) [VTag]
205    /// instance.
206    ///
207    /// Unlike [VTag::new()], this sets all the public fields of [VTag] in one call. This allows the
208    /// compiler to inline property and child list construction in the `html!` macro. This enables
209    /// higher instruction parallelism by reducing data dependency and avoids `memcpy` of Vtag
210    /// fields.
211    #[doc(hidden)]
212    pub fn __new_textarea(
213        value: Option<AttrValue>,
214        defaultvalue: Option<AttrValue>,
215        node_ref: NodeRef,
216        key: Option<Key>,
217        // at the bottom for more readable macro-expanded code
218        attributes: Attributes,
219        listeners: Listeners,
220    ) -> Self {
221        VTag::new_base(
222            VTagInner::Textarea(TextareaFields {
223                value: Value::new(value),
224                defaultvalue,
225            }),
226            node_ref,
227            key,
228            attributes,
229            listeners,
230        )
231    }
232
233    /// Creates a new [VTag] instance with `tag` name (cannot be changed later in DOM).
234    ///
235    /// Unlike [VTag::new()], this sets all the public fields of [VTag] in one call. This allows the
236    /// compiler to inline property and child list construction in the `html!` macro. This enables
237    /// higher instruction parallelism by reducing data dependency and avoids `memcpy` of Vtag
238    /// fields.
239    #[doc(hidden)]
240    pub fn __new_other(
241        tag: AttrValue,
242        node_ref: NodeRef,
243        key: Option<Key>,
244        // at the bottom for more readable macro-expanded code
245        attributes: Attributes,
246        listeners: Listeners,
247        children: VNode,
248    ) -> Self {
249        VTag::new_base(
250            VTagInner::Other { tag, children },
251            node_ref,
252            key,
253            attributes,
254            listeners,
255        )
256    }
257
258    /// Constructs a [VTag] from [VTagInner] and fields common to all [VTag] kinds
259    #[inline]
260    fn new_base(
261        inner: VTagInner,
262        node_ref: NodeRef,
263        key: Option<Key>,
264        attributes: Attributes,
265        listeners: Listeners,
266    ) -> Self {
267        VTag {
268            inner,
269            attributes,
270            listeners,
271            node_ref,
272            key,
273        }
274    }
275
276    /// Returns tag of an [Element](web_sys::Element). In HTML tags are always uppercase.
277    pub fn tag(&self) -> &str {
278        match &self.inner {
279            VTagInner::Input { .. } => "input",
280            VTagInner::Textarea { .. } => "textarea",
281            VTagInner::Other { tag, .. } => tag.as_ref(),
282        }
283    }
284
285    /// Add [VNode] child.
286    pub fn add_child(&mut self, child: VNode) {
287        if let VTagInner::Other { children, .. } = &mut self.inner {
288            children.to_vlist_mut().add_child(child)
289        }
290    }
291
292    /// Add multiple [VNode] children.
293    pub fn add_children(&mut self, children: impl IntoIterator<Item = VNode>) {
294        if let VTagInner::Other { children: dst, .. } = &mut self.inner {
295            dst.to_vlist_mut().add_children(children)
296        }
297    }
298
299    /// Returns a reference to the children of this [VTag], if the node can have
300    /// children
301    pub fn children(&self) -> Option<&VNode> {
302        match &self.inner {
303            VTagInner::Other { children, .. } => Some(children),
304            _ => None,
305        }
306    }
307
308    /// Returns a mutable reference to the children of this [VTag], if the node can have
309    /// children
310    pub fn children_mut(&mut self) -> Option<&mut VNode> {
311        match &mut self.inner {
312            VTagInner::Other { children, .. } => Some(children),
313            _ => None,
314        }
315    }
316
317    /// Returns the children of this [VTag], if the node can have
318    /// children
319    pub fn into_children(self) -> Option<VNode> {
320        match self.inner {
321            VTagInner::Other { children, .. } => Some(children),
322            _ => None,
323        }
324    }
325
326    /// Returns the `value` of an
327    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) or
328    /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)
329    pub fn value(&self) -> Option<&AttrValue> {
330        match &self.inner {
331            VTagInner::Input(f) => f.as_ref(),
332            VTagInner::Textarea(TextareaFields { value, .. }) => value.as_ref(),
333            VTagInner::Other { .. } => None,
334        }
335    }
336
337    /// Sets `value` for an
338    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) or
339    /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)
340    pub fn set_value(&mut self, value: impl IntoPropValue<Option<AttrValue>>) {
341        match &mut self.inner {
342            VTagInner::Input(f) => {
343                f.set(value.into_prop_value());
344            }
345            VTagInner::Textarea(TextareaFields { value: dst, .. }) => {
346                dst.set(value.into_prop_value());
347            }
348            VTagInner::Other { .. } => (),
349        }
350    }
351
352    /// Returns `checked` property of an
353    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
354    /// (Does not affect the value of the node's attribute).
355    pub fn checked(&self) -> Option<bool> {
356        match &self.inner {
357            VTagInner::Input(f) => f.checked,
358            _ => None,
359        }
360    }
361
362    /// Sets `checked` property of an
363    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
364    /// (Does not affect the value of the node's attribute).
365    pub fn set_checked(&mut self, value: bool) {
366        if let VTagInner::Input(f) = &mut self.inner {
367            f.checked = Some(value);
368        }
369    }
370
371    /// Keeps the current value of the `checked` property of an
372    /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
373    /// (Does not affect the value of the node's attribute).
374    pub fn preserve_checked(&mut self) {
375        if let VTagInner::Input(f) = &mut self.inner {
376            f.checked = None;
377        }
378    }
379
380    /// Adds a key-value pair to attributes
381    ///
382    /// Not every attribute works when it set as an attribute. We use workarounds for:
383    /// `value` and `checked`.
384    pub fn add_attribute(&mut self, key: &'static str, value: impl Into<AttrValue>) {
385        self.attributes.get_mut_index_map().insert(
386            AttrValue::Static(key),
387            AttributeOrProperty::Attribute(value.into()),
388        );
389    }
390
391    /// Set the given key as property on the element
392    ///
393    /// [`js_sys::Reflect`] is used for setting properties.
394    pub fn add_property(&mut self, key: &'static str, value: impl Into<JsValue>) {
395        self.attributes.get_mut_index_map().insert(
396            AttrValue::Static(key),
397            AttributeOrProperty::Property(value.into()),
398        );
399    }
400
401    /// Sets attributes to a virtual node.
402    ///
403    /// Not every attribute works when it set as an attribute. We use workarounds for:
404    /// `value` and `checked`.
405    pub fn set_attributes(&mut self, attrs: impl Into<Attributes>) {
406        self.attributes = attrs.into();
407    }
408
409    #[doc(hidden)]
410    pub fn __macro_push_attr(&mut self, key: &'static str, value: impl IntoPropValue<AttrValue>) {
411        self.attributes.get_mut_index_map().insert(
412            AttrValue::from(key),
413            AttributeOrProperty::Attribute(value.into_prop_value()),
414        );
415    }
416
417    /// Add event listener on the [VTag]'s  [Element](web_sys::Element).
418    /// Returns `true` if the listener has been added, `false` otherwise.
419    pub fn add_listener(&mut self, listener: Rc<dyn Listener>) -> bool {
420        match &mut self.listeners {
421            Listeners::None => {
422                self.set_listeners([Some(listener)].into());
423                true
424            }
425            Listeners::Pending(listeners) => {
426                let mut listeners = mem::take(listeners).into_vec();
427                listeners.push(Some(listener));
428
429                self.set_listeners(listeners.into());
430                true
431            }
432        }
433    }
434
435    /// Set event listeners on the [VTag]'s  [Element](web_sys::Element)
436    pub fn set_listeners(&mut self, listeners: Box<[Option<Rc<dyn Listener>>]>) {
437        self.listeners = Listeners::Pending(listeners);
438    }
439}
440
441impl PartialEq for VTag {
442    fn eq(&self, other: &VTag) -> bool {
443        use VTagInner::*;
444
445        (match (&self.inner, &other.inner) {
446            (Input(l), Input(r)) => l == r,
447            (Textarea (TextareaFields{ value: value_l, .. }), Textarea (TextareaFields{ value: value_r, .. })) => value_l == value_r,
448            (Other { tag: tag_l, .. }, Other { tag: tag_r, .. }) => tag_l == tag_r,
449            _ => false,
450        }) && self.listeners.eq(&other.listeners)
451            && self.attributes == other.attributes
452            // Diff children last, as recursion is the most expensive
453            && match (&self.inner, &other.inner) {
454                (Other { children: ch_l, .. }, Other { children: ch_r, .. }) => ch_l == ch_r,
455                _ => true,
456            }
457    }
458}
459
460#[cfg(feature = "ssr")]
461mod feat_ssr {
462    use std::fmt::Write;
463
464    use super::*;
465    use crate::feat_ssr::VTagKind;
466    use crate::html::AnyScope;
467    use crate::platform::fmt::BufWriter;
468    use crate::virtual_dom::VText;
469
470    // Elements that cannot have any child elements.
471    static VOID_ELEMENTS: &[&str; 15] = &[
472        "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
473        "source", "track", "wbr", "textarea",
474    ];
475
476    impl VTag {
477        pub(crate) async fn render_into_stream(
478            &self,
479            w: &mut BufWriter,
480            parent_scope: &AnyScope,
481            hydratable: bool,
482        ) {
483            let _ = w.write_str("<");
484            let _ = w.write_str(self.tag());
485
486            let write_attr = |w: &mut BufWriter, name: &str, val: Option<&str>| {
487                let _ = w.write_str(" ");
488                let _ = w.write_str(name);
489
490                if let Some(m) = val {
491                    let _ = w.write_str("=\"");
492                    let _ = w.write_str(&html_escape::encode_double_quoted_attribute(m));
493                    let _ = w.write_str("\"");
494                }
495            };
496
497            if let VTagInner::Input(InputFields { value, checked }) = &self.inner {
498                if let Some(value) = value.as_deref() {
499                    write_attr(w, "value", Some(value));
500                }
501
502                // Setting is as an attribute sets the `defaultChecked` property. Only emit this
503                // if it's explicitly set to checked.
504                if *checked == Some(true) {
505                    write_attr(w, "checked", None);
506                }
507            }
508
509            for (k, v) in self.attributes.iter() {
510                write_attr(w, k, Some(v));
511            }
512
513            let _ = w.write_str(">");
514
515            match &self.inner {
516                VTagInner::Input(_) => {}
517                VTagInner::Textarea(TextareaFields {
518                    value,
519                    defaultvalue,
520                }) => {
521                    if let Some(def) = value.as_ref().or(defaultvalue.as_ref()) {
522                        VText::new(def.clone())
523                            .render_into_stream(w, parent_scope, hydratable, VTagKind::Other)
524                            .await;
525                    }
526
527                    let _ = w.write_str("</textarea>");
528                }
529                VTagInner::Other { tag, children } => {
530                    let lowercase_tag = tag.to_ascii_lowercase();
531                    if !VOID_ELEMENTS.contains(&lowercase_tag.as_ref()) {
532                        children
533                            .render_into_stream(w, parent_scope, hydratable, tag.into())
534                            .await;
535
536                        let _ = w.write_str("</");
537                        let _ = w.write_str(tag);
538                        let _ = w.write_str(">");
539                    } else {
540                        // We don't write children of void elements nor closing tags.
541                        debug_assert!(
542                            match children {
543                                VNode::VList(m) => m.is_empty(),
544                                _ => false,
545                            },
546                            "{tag} cannot have any children!"
547                        );
548                    }
549                }
550            }
551        }
552    }
553}
554
555#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
556#[cfg(feature = "ssr")]
557#[cfg(test)]
558mod ssr_tests {
559    use tokio::test;
560
561    use crate::LocalServerRenderer as ServerRenderer;
562    use crate::prelude::*;
563
564    #[cfg_attr(not(target_os = "wasi"), test)]
565    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
566    async fn test_simple_tag() {
567        #[component]
568        fn Comp() -> Html {
569            html! { <div></div> }
570        }
571
572        let s = ServerRenderer::<Comp>::new()
573            .hydratable(false)
574            .render()
575            .await;
576
577        assert_eq!(s, "<div></div>");
578    }
579
580    #[cfg_attr(not(target_os = "wasi"), test)]
581    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
582    async fn test_simple_tag_with_attr() {
583        #[component]
584        fn Comp() -> Html {
585            html! { <div class="abc"></div> }
586        }
587
588        let s = ServerRenderer::<Comp>::new()
589            .hydratable(false)
590            .render()
591            .await;
592
593        assert_eq!(s, r#"<div class="abc"></div>"#);
594    }
595
596    #[cfg_attr(not(target_os = "wasi"), test)]
597    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
598    async fn test_simple_tag_with_content() {
599        #[component]
600        fn Comp() -> Html {
601            html! { <div>{"Hello!"}</div> }
602        }
603
604        let s = ServerRenderer::<Comp>::new()
605            .hydratable(false)
606            .render()
607            .await;
608
609        assert_eq!(s, r#"<div>Hello!</div>"#);
610    }
611
612    #[cfg_attr(not(target_os = "wasi"), test)]
613    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
614    async fn test_simple_tag_with_nested_tag_and_input() {
615        #[component]
616        fn Comp() -> Html {
617            html! { <div>{"Hello!"}<input value="abc" type="text" /></div> }
618        }
619
620        let s = ServerRenderer::<Comp>::new()
621            .hydratable(false)
622            .render()
623            .await;
624
625        assert_eq!(s, r#"<div>Hello!<input value="abc" type="text"></div>"#);
626    }
627
628    #[cfg_attr(not(target_os = "wasi"), test)]
629    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
630    async fn test_textarea() {
631        #[component]
632        fn Comp() -> Html {
633            html! { <textarea value="teststring" /> }
634        }
635
636        let s = ServerRenderer::<Comp>::new()
637            .hydratable(false)
638            .render()
639            .await;
640
641        assert_eq!(s, r#"<textarea>teststring</textarea>"#);
642    }
643
644    #[cfg_attr(not(target_os = "wasi"), test)]
645    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
646    async fn test_textarea_w_defaultvalue() {
647        #[component]
648        fn Comp() -> Html {
649            html! { <textarea defaultvalue="teststring" /> }
650        }
651
652        let s = ServerRenderer::<Comp>::new()
653            .hydratable(false)
654            .render()
655            .await;
656
657        assert_eq!(s, r#"<textarea>teststring</textarea>"#);
658    }
659
660    #[cfg_attr(not(target_os = "wasi"), test)]
661    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
662    async fn test_value_precedence_over_defaultvalue() {
663        #[component]
664        fn Comp() -> Html {
665            html! { <textarea defaultvalue="defaultvalue" value="value" /> }
666        }
667
668        let s = ServerRenderer::<Comp>::new()
669            .hydratable(false)
670            .render()
671            .await;
672
673        assert_eq!(s, r#"<textarea>value</textarea>"#);
674    }
675
676    #[cfg_attr(not(target_os = "wasi"), test)]
677    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
678    async fn test_escaping_in_style_tag() {
679        #[component]
680        fn Comp() -> Html {
681            html! { <style>{"body > a {color: #cc0;}"}</style> }
682        }
683
684        let s = ServerRenderer::<Comp>::new()
685            .hydratable(false)
686            .render()
687            .await;
688
689        assert_eq!(s, r#"<style>body > a {color: #cc0;}</style>"#);
690    }
691
692    #[cfg_attr(not(target_os = "wasi"), test)]
693    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
694    async fn test_escaping_in_script_tag() {
695        #[component]
696        fn Comp() -> Html {
697            html! { <script>{"foo.bar = x < y;"}</script> }
698        }
699
700        let s = ServerRenderer::<Comp>::new()
701            .hydratable(false)
702            .render()
703            .await;
704
705        assert_eq!(s, r#"<script>foo.bar = x < y;</script>"#);
706    }
707
708    #[cfg_attr(not(target_os = "wasi"), test)]
709    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
710    async fn test_multiple_vtext_in_style_tag() {
711        #[component]
712        fn Comp() -> Html {
713            let one = "html { background: black } ";
714            let two = "body > a { color: white } ";
715            html! {
716                <style>
717                    {one}
718                    {two}
719                </style>
720            }
721        }
722
723        let s = ServerRenderer::<Comp>::new()
724            .hydratable(false)
725            .render()
726            .await;
727
728        assert_eq!(
729            s,
730            r#"<style>html { background: black } body > a { color: white } </style>"#
731        );
732    }
733}