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