1use 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
14pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg";
16
17pub const MATHML_NAMESPACE: &str = "http://www.w3.org/1998/Math/MathML";
19
20pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
22
23#[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 fn new(value: Option<AttrValue>) -> Self {
45 Value(value, PhantomData)
46 }
47
48 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#[derive(Debug, Clone, ImplicitClone, Default, Eq, PartialEq)]
66pub(crate) struct InputFields {
67 pub(crate) value: Value<InputElement>,
70 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 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 pub(crate) value: Value<TextAreaElement>,
107 #[allow(unused)] pub(crate) defaultvalue: Option<AttrValue>,
111}
112
113#[derive(Debug, Clone, ImplicitClone)]
116pub(crate) enum VTagInner {
117 Input(InputFields),
121 Textarea(TextareaFields),
125 Other {
127 tag: AttrValue,
129 children: VNode,
131 },
132}
133
134#[derive(Debug, Clone, ImplicitClone)]
138pub struct VTag {
139 pub(crate) inner: VTagInner,
141 pub(crate) listeners: Listeners,
143 pub node_ref: NodeRef,
145 pub attributes: Attributes,
147 pub key: Option<Key>,
148}
149
150impl VTag {
151 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 #[doc(hidden)]
180 pub fn __new_input(
181 value: Option<AttrValue>,
182 checked: Option<bool>,
183 node_ref: NodeRef,
184 key: Option<Key>,
185 attributes: Attributes,
187 listeners: Listeners,
188 ) -> Self {
189 VTag::new_base(
190 VTagInner::Input(InputFields::new(
191 value,
192 checked,
195 )),
196 node_ref,
197 key,
198 attributes,
199 listeners,
200 )
201 }
202
203 #[doc(hidden)]
212 pub fn __new_textarea(
213 value: Option<AttrValue>,
214 defaultvalue: Option<AttrValue>,
215 node_ref: NodeRef,
216 key: Option<Key>,
217 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 #[doc(hidden)]
240 pub fn __new_other(
241 tag: AttrValue,
242 node_ref: NodeRef,
243 key: Option<Key>,
244 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 #[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 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 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 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 pub fn children(&self) -> Option<&VNode> {
302 match &self.inner {
303 VTagInner::Other { children, .. } => Some(children),
304 _ => None,
305 }
306 }
307
308 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 pub fn into_children(self) -> Option<VNode> {
320 match self.inner {
321 VTagInner::Other { children, .. } => Some(children),
322 _ => None,
323 }
324 }
325
326 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 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 pub fn checked(&self) -> Option<bool> {
356 match &self.inner {
357 VTagInner::Input(f) => f.checked,
358 _ => None,
359 }
360 }
361
362 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 pub fn preserve_checked(&mut self) {
375 if let VTagInner::Input(f) = &mut self.inner {
376 f.checked = None;
377 }
378 }
379
380 #[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 #[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 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 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 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 && 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 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 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 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}