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, Default, Eq, PartialEq)]
67pub(crate) struct InputFields {
68 pub(crate) value: Value<InputElement>,
71 pub(crate) checked: Option<bool>,
77}
78
79impl ImplicitClone for InputFields {}
80
81impl Deref for InputFields {
82 type Target = Value<InputElement>;
83
84 fn deref(&self) -> &Self::Target {
85 &self.value
86 }
87}
88
89impl DerefMut for InputFields {
90 fn deref_mut(&mut self) -> &mut Self::Target {
91 &mut self.value
92 }
93}
94
95impl InputFields {
96 fn new(value: Option<AttrValue>, checked: Option<bool>) -> Self {
98 Self {
99 value: Value::new(value),
100 checked,
101 }
102 }
103}
104
105#[derive(Debug, Clone, Default)]
106pub(crate) struct TextareaFields {
107 pub(crate) value: Value<TextAreaElement>,
110 #[allow(unused)] pub(crate) defaultvalue: Option<AttrValue>,
114}
115
116#[derive(Debug, Clone)]
119pub(crate) enum VTagInner {
120 Input(InputFields),
124 Textarea(TextareaFields),
128 Other {
130 tag: AttrValue,
132 children: VNode,
134 },
135}
136
137impl ImplicitClone for VTagInner {}
138
139#[derive(Debug, Clone)]
143pub struct VTag {
144 pub(crate) inner: VTagInner,
146 pub(crate) listeners: Listeners,
148 pub node_ref: NodeRef,
150 pub attributes: Attributes,
152 pub key: Option<Key>,
153}
154
155impl ImplicitClone for VTag {}
156
157impl VTag {
158 pub fn new(tag: impl Into<AttrValue>) -> Self {
160 let tag = tag.into();
161 Self::new_base(
162 match &*tag.to_ascii_lowercase() {
163 "input" => VTagInner::Input(Default::default()),
164 "textarea" => VTagInner::Textarea(Default::default()),
165 _ => VTagInner::Other {
166 tag,
167 children: Default::default(),
168 },
169 },
170 Default::default(),
171 Default::default(),
172 Default::default(),
173 Default::default(),
174 )
175 }
176
177 #[doc(hidden)]
186 #[allow(clippy::too_many_arguments)]
187 pub fn __new_input(
188 value: Option<AttrValue>,
189 checked: Option<bool>,
190 node_ref: NodeRef,
191 key: Option<Key>,
192 attributes: Attributes,
194 listeners: Listeners,
195 ) -> Self {
196 VTag::new_base(
197 VTagInner::Input(InputFields::new(
198 value,
199 checked,
202 )),
203 node_ref,
204 key,
205 attributes,
206 listeners,
207 )
208 }
209
210 #[doc(hidden)]
219 #[allow(clippy::too_many_arguments)]
220 pub fn __new_textarea(
221 value: Option<AttrValue>,
222 defaultvalue: Option<AttrValue>,
223 node_ref: NodeRef,
224 key: Option<Key>,
225 attributes: Attributes,
227 listeners: Listeners,
228 ) -> Self {
229 VTag::new_base(
230 VTagInner::Textarea(TextareaFields {
231 value: Value::new(value),
232 defaultvalue,
233 }),
234 node_ref,
235 key,
236 attributes,
237 listeners,
238 )
239 }
240
241 #[doc(hidden)]
248 #[allow(clippy::too_many_arguments)]
249 pub fn __new_other(
250 tag: AttrValue,
251 node_ref: NodeRef,
252 key: Option<Key>,
253 attributes: Attributes,
255 listeners: Listeners,
256 children: VNode,
257 ) -> Self {
258 VTag::new_base(
259 VTagInner::Other { tag, children },
260 node_ref,
261 key,
262 attributes,
263 listeners,
264 )
265 }
266
267 #[inline]
269 #[allow(clippy::too_many_arguments)]
270 fn new_base(
271 inner: VTagInner,
272 node_ref: NodeRef,
273 key: Option<Key>,
274 attributes: Attributes,
275 listeners: Listeners,
276 ) -> Self {
277 VTag {
278 inner,
279 attributes,
280 listeners,
281 node_ref,
282 key,
283 }
284 }
285
286 pub fn tag(&self) -> &str {
288 match &self.inner {
289 VTagInner::Input { .. } => "input",
290 VTagInner::Textarea { .. } => "textarea",
291 VTagInner::Other { tag, .. } => tag.as_ref(),
292 }
293 }
294
295 pub fn add_child(&mut self, child: VNode) {
297 if let VTagInner::Other { children, .. } = &mut self.inner {
298 children.to_vlist_mut().add_child(child)
299 }
300 }
301
302 pub fn add_children(&mut self, children: impl IntoIterator<Item = VNode>) {
304 if let VTagInner::Other { children: dst, .. } = &mut self.inner {
305 dst.to_vlist_mut().add_children(children)
306 }
307 }
308
309 pub fn children(&self) -> Option<&VNode> {
312 match &self.inner {
313 VTagInner::Other { children, .. } => Some(children),
314 _ => None,
315 }
316 }
317
318 pub fn children_mut(&mut self) -> Option<&mut VNode> {
321 match &mut self.inner {
322 VTagInner::Other { children, .. } => Some(children),
323 _ => None,
324 }
325 }
326
327 pub fn into_children(self) -> Option<VNode> {
330 match self.inner {
331 VTagInner::Other { children, .. } => Some(children),
332 _ => None,
333 }
334 }
335
336 pub fn value(&self) -> Option<&AttrValue> {
340 match &self.inner {
341 VTagInner::Input(f) => f.as_ref(),
342 VTagInner::Textarea(TextareaFields { value, .. }) => value.as_ref(),
343 VTagInner::Other { .. } => None,
344 }
345 }
346
347 pub fn set_value(&mut self, value: impl IntoPropValue<Option<AttrValue>>) {
351 match &mut self.inner {
352 VTagInner::Input(f) => {
353 f.set(value.into_prop_value());
354 }
355 VTagInner::Textarea(TextareaFields { value: dst, .. }) => {
356 dst.set(value.into_prop_value());
357 }
358 VTagInner::Other { .. } => (),
359 }
360 }
361
362 pub fn checked(&self) -> Option<bool> {
366 match &self.inner {
367 VTagInner::Input(f) => f.checked,
368 _ => None,
369 }
370 }
371
372 pub fn set_checked(&mut self, value: bool) {
376 if let VTagInner::Input(f) = &mut self.inner {
377 f.checked = Some(value);
378 }
379 }
380
381 pub fn preserve_checked(&mut self) {
385 if let VTagInner::Input(f) = &mut self.inner {
386 f.checked = None;
387 }
388 }
389
390 pub fn add_attribute(&mut self, key: &'static str, value: impl Into<AttrValue>) {
395 self.attributes.get_mut_index_map().insert(
396 AttrValue::Static(key),
397 AttributeOrProperty::Attribute(value.into()),
398 );
399 }
400
401 pub fn add_property(&mut self, key: &'static str, value: impl Into<JsValue>) {
405 self.attributes.get_mut_index_map().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 self.attributes.get_mut_index_map().insert(
422 AttrValue::from(key),
423 AttributeOrProperty::Attribute(value.into_prop_value()),
424 );
425 }
426
427 pub fn add_listener(&mut self, listener: Rc<dyn Listener>) -> bool {
430 match &mut self.listeners {
431 Listeners::None => {
432 self.set_listeners([Some(listener)].into());
433 true
434 }
435 Listeners::Pending(listeners) => {
436 let mut listeners = mem::take(listeners).into_vec();
437 listeners.push(Some(listener));
438
439 self.set_listeners(listeners.into());
440 true
441 }
442 }
443 }
444
445 pub fn set_listeners(&mut self, listeners: Box<[Option<Rc<dyn Listener>>]>) {
447 self.listeners = Listeners::Pending(listeners);
448 }
449}
450
451impl PartialEq for VTag {
452 fn eq(&self, other: &VTag) -> bool {
453 use VTagInner::*;
454
455 (match (&self.inner, &other.inner) {
456 (Input(l), Input(r)) => l == r,
457 (Textarea (TextareaFields{ value: value_l, .. }), Textarea (TextareaFields{ value: value_r, .. })) => value_l == value_r,
458 (Other { tag: tag_l, .. }, Other { tag: tag_r, .. }) => tag_l == tag_r,
459 _ => false,
460 }) && self.listeners.eq(&other.listeners)
461 && self.attributes == other.attributes
462 && match (&self.inner, &other.inner) {
464 (Other { children: ch_l, .. }, Other { children: ch_r, .. }) => ch_l == ch_r,
465 _ => true,
466 }
467 }
468}
469
470#[cfg(feature = "ssr")]
471mod feat_ssr {
472 use std::fmt::Write;
473
474 use super::*;
475 use crate::feat_ssr::VTagKind;
476 use crate::html::AnyScope;
477 use crate::platform::fmt::BufWriter;
478 use crate::virtual_dom::VText;
479
480 static VOID_ELEMENTS: &[&str; 15] = &[
482 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
483 "source", "track", "wbr", "textarea",
484 ];
485
486 impl VTag {
487 pub(crate) async fn render_into_stream(
488 &self,
489 w: &mut BufWriter,
490 parent_scope: &AnyScope,
491 hydratable: bool,
492 ) {
493 let _ = w.write_str("<");
494 let _ = w.write_str(self.tag());
495
496 let write_attr = |w: &mut BufWriter, name: &str, val: Option<&str>| {
497 let _ = w.write_str(" ");
498 let _ = w.write_str(name);
499
500 if let Some(m) = val {
501 let _ = w.write_str("=\"");
502 let _ = w.write_str(&html_escape::encode_double_quoted_attribute(m));
503 let _ = w.write_str("\"");
504 }
505 };
506
507 if let VTagInner::Input(InputFields { value, checked }) = &self.inner {
508 if let Some(value) = value.as_deref() {
509 write_attr(w, "value", Some(value));
510 }
511
512 if *checked == Some(true) {
515 write_attr(w, "checked", None);
516 }
517 }
518
519 for (k, v) in self.attributes.iter() {
520 write_attr(w, k, Some(v));
521 }
522
523 let _ = w.write_str(">");
524
525 match &self.inner {
526 VTagInner::Input(_) => {}
527 VTagInner::Textarea(TextareaFields {
528 value,
529 defaultvalue,
530 }) => {
531 if let Some(def) = value.as_ref().or(defaultvalue.as_ref()) {
532 VText::new(def.clone())
533 .render_into_stream(w, parent_scope, hydratable, VTagKind::Other)
534 .await;
535 }
536
537 let _ = w.write_str("</textarea>");
538 }
539 VTagInner::Other { tag, children } => {
540 if !VOID_ELEMENTS.contains(&tag.as_ref()) {
541 children
542 .render_into_stream(w, parent_scope, hydratable, tag.into())
543 .await;
544
545 let _ = w.write_str("</");
546 let _ = w.write_str(tag);
547 let _ = w.write_str(">");
548 } else {
549 debug_assert!(
551 match children {
552 VNode::VList(m) => m.is_empty(),
553 _ => false,
554 },
555 "{tag} cannot have any children!"
556 );
557 }
558 }
559 }
560 }
561 }
562}
563
564#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
565#[cfg(feature = "ssr")]
566#[cfg(test)]
567mod ssr_tests {
568 use tokio::test;
569
570 use crate::prelude::*;
571 use crate::LocalServerRenderer as ServerRenderer;
572
573 #[cfg_attr(not(target_os = "wasi"), test)]
574 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
575 async fn test_simple_tag() {
576 #[function_component]
577 fn Comp() -> Html {
578 html! { <div></div> }
579 }
580
581 let s = ServerRenderer::<Comp>::new()
582 .hydratable(false)
583 .render()
584 .await;
585
586 assert_eq!(s, "<div></div>");
587 }
588
589 #[cfg_attr(not(target_os = "wasi"), test)]
590 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
591 async fn test_simple_tag_with_attr() {
592 #[function_component]
593 fn Comp() -> Html {
594 html! { <div class="abc"></div> }
595 }
596
597 let s = ServerRenderer::<Comp>::new()
598 .hydratable(false)
599 .render()
600 .await;
601
602 assert_eq!(s, r#"<div class="abc"></div>"#);
603 }
604
605 #[cfg_attr(not(target_os = "wasi"), test)]
606 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
607 async fn test_simple_tag_with_content() {
608 #[function_component]
609 fn Comp() -> Html {
610 html! { <div>{"Hello!"}</div> }
611 }
612
613 let s = ServerRenderer::<Comp>::new()
614 .hydratable(false)
615 .render()
616 .await;
617
618 assert_eq!(s, r#"<div>Hello!</div>"#);
619 }
620
621 #[cfg_attr(not(target_os = "wasi"), test)]
622 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
623 async fn test_simple_tag_with_nested_tag_and_input() {
624 #[function_component]
625 fn Comp() -> Html {
626 html! { <div>{"Hello!"}<input value="abc" type="text" /></div> }
627 }
628
629 let s = ServerRenderer::<Comp>::new()
630 .hydratable(false)
631 .render()
632 .await;
633
634 assert_eq!(s, r#"<div>Hello!<input value="abc" type="text"></div>"#);
635 }
636
637 #[cfg_attr(not(target_os = "wasi"), test)]
638 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
639 async fn test_textarea() {
640 #[function_component]
641 fn Comp() -> Html {
642 html! { <textarea value="teststring" /> }
643 }
644
645 let s = ServerRenderer::<Comp>::new()
646 .hydratable(false)
647 .render()
648 .await;
649
650 assert_eq!(s, r#"<textarea>teststring</textarea>"#);
651 }
652
653 #[cfg_attr(not(target_os = "wasi"), test)]
654 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
655 async fn test_textarea_w_defaultvalue() {
656 #[function_component]
657 fn Comp() -> Html {
658 html! { <textarea defaultvalue="teststring" /> }
659 }
660
661 let s = ServerRenderer::<Comp>::new()
662 .hydratable(false)
663 .render()
664 .await;
665
666 assert_eq!(s, r#"<textarea>teststring</textarea>"#);
667 }
668
669 #[cfg_attr(not(target_os = "wasi"), test)]
670 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
671 async fn test_value_precedence_over_defaultvalue() {
672 #[function_component]
673 fn Comp() -> Html {
674 html! { <textarea defaultvalue="defaultvalue" value="value" /> }
675 }
676
677 let s = ServerRenderer::<Comp>::new()
678 .hydratable(false)
679 .render()
680 .await;
681
682 assert_eq!(s, r#"<textarea>value</textarea>"#);
683 }
684
685 #[cfg_attr(not(target_os = "wasi"), test)]
686 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
687 async fn test_escaping_in_style_tag() {
688 #[function_component]
689 fn Comp() -> Html {
690 html! { <style>{"body > a {color: #cc0;}"}</style> }
691 }
692
693 let s = ServerRenderer::<Comp>::new()
694 .hydratable(false)
695 .render()
696 .await;
697
698 assert_eq!(s, r#"<style>body > a {color: #cc0;}</style>"#);
699 }
700
701 #[cfg_attr(not(target_os = "wasi"), test)]
702 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
703 async fn test_escaping_in_script_tag() {
704 #[function_component]
705 fn Comp() -> Html {
706 html! { <script>{"foo.bar = x < y;"}</script> }
707 }
708
709 let s = ServerRenderer::<Comp>::new()
710 .hydratable(false)
711 .render()
712 .await;
713
714 assert_eq!(s, r#"<script>foo.bar = x < y;</script>"#);
715 }
716
717 #[cfg_attr(not(target_os = "wasi"), test)]
718 #[cfg_attr(target_os = "wasi", test(flavor = "current_thread"))]
719 async fn test_multiple_vtext_in_style_tag() {
720 #[function_component]
721 fn Comp() -> Html {
722 let one = "html { background: black } ";
723 let two = "body > a { color: white } ";
724 html! {
725 <style>
726 {one}
727 {two}
728 </style>
729 }
730 }
731
732 let s = ServerRenderer::<Comp>::new()
733 .hydratable(false)
734 .render()
735 .await;
736
737 assert_eq!(
738 s,
739 r#"<style>html { background: black } body > a { color: white } </style>"#
740 );
741 }
742}