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    #[track_caller]
385    pub fn add_attribute(&mut self, key: &'static str, value: impl Into<AttrValue>) {
386        assert!(
387            Attributes::is_valid_attr_key(key),
388            "{key:?} is not a valid attribute name"
389        );
390        self.attributes.get_mut_index_map_unchecked().insert(
391            AttrValue::Static(key),
392            AttributeOrProperty::Attribute(value.into()),
393        );
394    }
395
396    /// Set the given key as property on the element
397    ///
398    /// [`js_sys::Reflect`] is used for setting properties.
399    #[track_caller]
400    pub fn add_property(&mut self, key: &'static str, value: impl Into<JsValue>) {
401        assert!(
402            Attributes::is_valid_attr_key(key),
403            "{key:?} is not a valid attribute name"
404        );
405        self.attributes.get_mut_index_map_unchecked().insert(
406            AttrValue::Static(key),
407            AttributeOrProperty::Property(value.into()),
408        );
409    }
410
411    /// Sets attributes to a virtual node.
412    ///
413    /// Not every attribute works when it set as an attribute. We use workarounds for:
414    /// `value` and `checked`.
415    pub fn set_attributes(&mut self, attrs: impl Into<Attributes>) {
416        self.attributes = attrs.into();
417    }
418
419    #[doc(hidden)]
420    pub fn __macro_push_attr(&mut self, key: &'static str, value: impl IntoPropValue<AttrValue>) {
421        debug_assert!(
422            Attributes::is_valid_attr_key(key),
423            "{key:?} is not a valid attribute name"
424        );
425        self.attributes.get_mut_index_map_unchecked().insert(
426            AttrValue::from(key),
427            AttributeOrProperty::Attribute(value.into_prop_value()),
428        );
429    }
430
431    /// Add event listener on the [VTag]'s  [Element](web_sys::Element).
432    /// Returns `true` if the listener has been added, `false` otherwise.
433    pub fn add_listener(&mut self, listener: Rc<dyn Listener>) -> bool {
434        match &mut self.listeners {
435            Listeners::None => {
436                self.set_listeners([Some(listener)].into());
437                true
438            }
439            Listeners::Pending(listeners) => {
440                let mut listeners = mem::take(listeners).into_vec();
441                listeners.push(Some(listener));
442
443                self.set_listeners(listeners.into());
444                true
445            }
446        }
447    }
448
449    /// Set event listeners on the [VTag]'s  [Element](web_sys::Element)
450    pub fn set_listeners(&mut self, listeners: Box<[Option<Rc<dyn Listener>>]>) {
451        self.listeners = Listeners::Pending(listeners);
452    }
453}
454
455impl PartialEq for VTag {
456    fn eq(&self, other: &VTag) -> bool {
457        use VTagInner::*;
458
459        (match (&self.inner, &other.inner) {
460            (Input(l), Input(r)) => l == r,
461            (Textarea (TextareaFields{ value: value_l, .. }), Textarea (TextareaFields{ value: value_r, .. })) => value_l == value_r,
462            (Other { tag: tag_l, .. }, Other { tag: tag_r, .. }) => tag_l == tag_r,
463            _ => false,
464        }) && self.listeners.eq(&other.listeners)
465            && self.attributes == other.attributes
466            // Diff children last, as recursion is the most expensive
467            && match (&self.inner, &other.inner) {
468                (Other { children: ch_l, .. }, Other { children: ch_r, .. }) => ch_l == ch_r,
469                _ => true,
470            }
471    }
472}
473
474#[cfg(feature = "ssr")]
475mod feat_ssr {
476    use std::fmt::Write;
477
478    use super::*;
479    use crate::feat_ssr::VTagKind;
480    use crate::html::AnyScope;
481    use crate::platform::fmt::BufWriter;
482    use crate::virtual_dom::VText;
483
484    // Elements that cannot have any child elements.
485    static VOID_ELEMENTS: &[&str; 15] = &[
486        "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
487        "source", "track", "wbr", "textarea",
488    ];
489
490    impl VTag {
491        pub(crate) async fn render_into_stream(
492            &self,
493            w: &mut BufWriter,
494            parent_scope: &AnyScope,
495            hydratable: bool,
496        ) {
497            let _ = w.write_str("<");
498            let _ = w.write_str(self.tag());
499
500            let write_attr = |w: &mut BufWriter, name: &str, val: Option<&str>| {
501                let _ = w.write_str(" ");
502                let _ = w.write_str(name);
503
504                if let Some(m) = val {
505                    let _ = w.write_str("=\"");
506                    let _ = w.write_str(&html_escape::encode_double_quoted_attribute(m));
507                    let _ = w.write_str("\"");
508                }
509            };
510
511            if let VTagInner::Input(InputFields { value, checked }) = &self.inner {
512                if let Some(value) = value.as_deref() {
513                    write_attr(w, "value", Some(value));
514                }
515
516                // Setting is as an attribute sets the `defaultChecked` property. Only emit this
517                // if it's explicitly set to checked.
518                if *checked == Some(true) {
519                    write_attr(w, "checked", None);
520                }
521            }
522
523            for (k, v) in self.attributes.iter() {
524                write_attr(w, k, Some(v));
525            }
526
527            let _ = w.write_str(">");
528
529            match &self.inner {
530                VTagInner::Input(_) => {}
531                VTagInner::Textarea(TextareaFields {
532                    value,
533                    defaultvalue,
534                }) => {
535                    if let Some(def) = value.as_ref().or(defaultvalue.as_ref()) {
536                        VText::new(def.clone())
537                            .render_into_stream(w, parent_scope, hydratable, VTagKind::Other)
538                            .await;
539                    }
540
541                    let _ = w.write_str("</textarea>");
542                }
543                VTagInner::Other { tag, children } => {
544                    let lowercase_tag = tag.to_ascii_lowercase();
545                    if !VOID_ELEMENTS.contains(&lowercase_tag.as_ref()) {
546                        children
547                            .render_into_stream(w, parent_scope, hydratable, tag.into())
548                            .await;
549
550                        let _ = w.write_str("</");
551                        let _ = w.write_str(tag);
552                        let _ = w.write_str(">");
553                    } else {
554                        // We don't write children of void elements nor closing tags.
555                        debug_assert!(
556                            match children {
557                                VNode::VList(m) => m.is_empty(),
558                                _ => false,
559                            },
560                            "{tag} cannot have any children!"
561                        );
562                    }
563                }
564            }
565        }
566    }
567}
568
569#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
570#[cfg(feature = "ssr")]
571#[cfg(test)]
572mod ssr_tests {
573    use tokio::test;
574
575    use crate::LocalServerRenderer as ServerRenderer;
576    use crate::prelude::*;
577
578    #[cfg_attr(not(target_os = "wasi"), test)]
579    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
580    async fn test_simple_tag() {
581        #[component]
582        fn Comp() -> Html {
583            html! { <div></div> }
584        }
585
586        let s = ServerRenderer::<Comp>::new()
587            .hydratable(false)
588            .render()
589            .await;
590
591        assert_eq!(s, "<div></div>");
592    }
593
594    #[cfg_attr(not(target_os = "wasi"), test)]
595    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
596    async fn test_simple_tag_with_attr() {
597        #[component]
598        fn Comp() -> Html {
599            html! { <div class="abc"></div> }
600        }
601
602        let s = ServerRenderer::<Comp>::new()
603            .hydratable(false)
604            .render()
605            .await;
606
607        assert_eq!(s, r#"<div class="abc"></div>"#);
608    }
609
610    #[cfg_attr(not(target_os = "wasi"), test)]
611    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
612    async fn test_simple_tag_with_content() {
613        #[component]
614        fn Comp() -> Html {
615            html! { <div>{"Hello!"}</div> }
616        }
617
618        let s = ServerRenderer::<Comp>::new()
619            .hydratable(false)
620            .render()
621            .await;
622
623        assert_eq!(s, r#"<div>Hello!</div>"#);
624    }
625
626    #[cfg_attr(not(target_os = "wasi"), test)]
627    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
628    async fn test_simple_tag_with_nested_tag_and_input() {
629        #[component]
630        fn Comp() -> Html {
631            html! { <div>{"Hello!"}<input value="abc" type="text" /></div> }
632        }
633
634        let s = ServerRenderer::<Comp>::new()
635            .hydratable(false)
636            .render()
637            .await;
638
639        assert_eq!(s, r#"<div>Hello!<input value="abc" type="text"></div>"#);
640    }
641
642    #[cfg_attr(not(target_os = "wasi"), test)]
643    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
644    async fn test_textarea() {
645        #[component]
646        fn Comp() -> Html {
647            html! { <textarea value="teststring" /> }
648        }
649
650        let s = ServerRenderer::<Comp>::new()
651            .hydratable(false)
652            .render()
653            .await;
654
655        assert_eq!(s, r#"<textarea>teststring</textarea>"#);
656    }
657
658    #[cfg_attr(not(target_os = "wasi"), test)]
659    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
660    async fn test_textarea_w_defaultvalue() {
661        #[component]
662        fn Comp() -> Html {
663            html! { <textarea defaultvalue="teststring" /> }
664        }
665
666        let s = ServerRenderer::<Comp>::new()
667            .hydratable(false)
668            .render()
669            .await;
670
671        assert_eq!(s, r#"<textarea>teststring</textarea>"#);
672    }
673
674    #[cfg_attr(not(target_os = "wasi"), test)]
675    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
676    async fn test_value_precedence_over_defaultvalue() {
677        #[component]
678        fn Comp() -> Html {
679            html! { <textarea defaultvalue="defaultvalue" value="value" /> }
680        }
681
682        let s = ServerRenderer::<Comp>::new()
683            .hydratable(false)
684            .render()
685            .await;
686
687        assert_eq!(s, r#"<textarea>value</textarea>"#);
688    }
689
690    #[cfg_attr(not(target_os = "wasi"), test)]
691    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
692    async fn test_escaping_in_style_tag() {
693        #[component]
694        fn Comp() -> Html {
695            html! { <style>{"body > a {color: #cc0;}"}</style> }
696        }
697
698        let s = ServerRenderer::<Comp>::new()
699            .hydratable(false)
700            .render()
701            .await;
702
703        assert_eq!(s, r#"<style>body > a {color: #cc0;}</style>"#);
704    }
705
706    #[cfg_attr(not(target_os = "wasi"), test)]
707    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
708    async fn test_escaping_in_script_tag() {
709        #[component]
710        fn Comp() -> Html {
711            html! { <script>{"foo.bar = x < y;"}</script> }
712        }
713
714        let s = ServerRenderer::<Comp>::new()
715            .hydratable(false)
716            .render()
717            .await;
718
719        assert_eq!(s, r#"<script>foo.bar = x < y;</script>"#);
720    }
721
722    #[cfg_attr(not(target_os = "wasi"), test)]
723    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
724    async fn test_multiple_vtext_in_style_tag() {
725        #[component]
726        fn Comp() -> Html {
727            let one = "html { background: black } ";
728            let two = "body > a { color: white } ";
729            html! {
730                <style>
731                    {one}
732                    {two}
733                </style>
734            }
735        }
736
737        let s = ServerRenderer::<Comp>::new()
738            .hydratable(false)
739            .render()
740            .await;
741
742        assert_eq!(
743            s,
744            r#"<style>html { background: black } body > a { color: white } </style>"#
745        );
746    }
747
748    #[cfg_attr(not(target_os = "wasi"), test)]
749    #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
750    #[should_panic(expected = "\"x onerror=\\\"prompt('failed')\" is not a valid attribute name")]
751    async fn test_should_panic_invalid_attr() {
752        #[component]
753        fn Comp() -> Html {
754            html! { <div "x onerror=\"prompt('failed')"=";"></div> }
755        }
756
757        ServerRenderer::<Comp>::new()
758            .hydratable(false)
759            .render()
760            .await;
761    }
762}