1use 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
15pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg";
17
18pub const MATHML_NAMESPACE: &str = "http://www.w3.org/1998/Math/MathML";
20
21pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
23
24#[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 fn new(value: Option<AttrValue>) -> Self {
46 Value(value, PhantomData)
47 }
48
49 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#[derive(Debug, Clone, ImplicitClone, Default, Eq, PartialEq)]
67pub(crate) struct InputFields {
68 pub(crate) value: Value<InputElement>,
71 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 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 pub(crate) value: Value<TextAreaElement>,
108 #[allow(unused)] pub(crate) defaultvalue: Option<AttrValue>,
112}
113
114#[derive(Debug, Clone, ImplicitClone)]
117pub(crate) enum VTagInner {
118 Input(InputFields),
122 Textarea(TextareaFields),
126 Other {
128 tag: AttrValue,
130 children: VNode,
132 },
133}
134
135#[derive(Debug, Clone, ImplicitClone)]
139pub struct VTag {
140 pub(crate) inner: VTagInner,
142 pub(crate) listeners: Listeners,
144 pub node_ref: NodeRef,
146 pub attributes: Attributes,
148 pub key: Option<Key>,
149}
150
151impl VTag {
152 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 #[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 attributes: Attributes,
188 listeners: Listeners,
189 ) -> Self {
190 VTag::new_base(
191 VTagInner::Input(InputFields::new(
192 value,
193 checked,
196 )),
197 node_ref,
198 key,
199 attributes,
200 listeners,
201 )
202 }
203
204 #[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 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 #[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 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 #[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 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 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 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 pub fn children(&self) -> Option<&VNode> {
306 match &self.inner {
307 VTagInner::Other { children, .. } => Some(children),
308 _ => None,
309 }
310 }
311
312 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 pub fn into_children(self) -> Option<VNode> {
324 match self.inner {
325 VTagInner::Other { children, .. } => Some(children),
326 _ => None,
327 }
328 }
329
330 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 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 pub fn checked(&self) -> Option<bool> {
360 match &self.inner {
361 VTagInner::Input(f) => f.checked,
362 _ => None,
363 }
364 }
365
366 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 pub fn preserve_checked(&mut self) {
379 if let VTagInner::Input(f) = &mut self.inner {
380 f.checked = None;
381 }
382 }
383
384 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 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 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 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 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 && 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 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 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 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}