1pub mod an_introduction;
40pub mod event;
41pub mod utils;
42pub mod view;
43pub use mogwai_macros::{builder, html, rsx};
44
45pub mod core {
46 pub use mogwai::*;
48}
49
50pub mod prelude {
51 pub use super::{event::*, utils::*, view::*};
53 pub use mogwai::prelude::*;
54 pub use std::convert::TryFrom;
55}
56
57#[cfg(doctest)]
58doc_comment::doctest!("../../../README.md", readme);
59
60#[cfg(all(test, not(target_arch = "wasm32")))]
61mod nonwasm {
62 use std::sync::Arc;
63
64 use async_executor::Executor;
65
66 use crate as mogwai_dom;
67 use crate::{
68 core::{
69 channel::{broadcast, mpsc},
70 time::repeat_times,
71 },
72 prelude::*,
73 }; #[test]
76 fn component_nest() {
77 let click_output = mogwai::relay::Output::default();
78 let my_button_component = rsx! {
79 button(on:click = click_output.sink().contra_map(|_: AnyEvent| ())) {"Click me!"}
80 }
81 .with_task(async move {
82 loop {
83 if let Some(()) = click_output.get().await {
84 println!("click received");
85 } else {
86 println!("click event stream was dropped");
87 break;
88 }
89 }
90 });
91
92 let _my_div = Dom::try_from(rsx! {
93 div() {
94 h1 { "Click to print a line" }
95 {my_button_component}
96 }
97 })
98 .unwrap();
99 }
100
101 #[test]
102 fn capture_view_channel_md() {
103 use mogwai_dom::{core::channel::broadcast, prelude::*};
105
106 futures::executor::block_on(async {
107 println!("using channels");
108 let (tx, mut rx) = broadcast::bounded::<Dom>(1.try_into().unwrap());
109
110 let builder = rsx! {
111 div(){
112 button(capture:view = tx) { "Click" }
113 }
114 };
115
116 let div = Dom::try_from(builder).unwrap();
117
118 div.run_while(async move {
119 let _button: Dom = rx.next().await.unwrap();
120 })
121 .await
122 .unwrap();
123 });
124 }
126
127 #[test]
128 fn capture_view_captured_md() {
129 use mogwai_dom::prelude::*;
131
132 futures::executor::block_on(async {
133 println!("using captured");
134 let captured: Captured<Dom> = Captured::default();
135
136 let builder = html! {
137 <div><button capture:view = captured.sink()>"Click"</button></div>
138 };
139
140 let div = Dom::try_from(builder).unwrap();
141
142 div.run_while(async move {
143 let _button: Dom = captured.get().await;
144 })
145 .await
146 .unwrap();
147 });
148 }
150
151 #[test]
152 fn capture_view() {
153 futures::executor::block_on(async move {
154 let (tx, mut rx) = broadcast::bounded::<SsrDom>(1.try_into().unwrap());
155 let view = SsrDom::try_from(html! {
156 <div>
157 <pre
158 capture:view = tx >
159 "Tack :)"
160 </pre>
161 </div>
162 })
163 .unwrap();
164
165 view.executor
166 .run(async {
167 let dom = rx.next().await.unwrap();
168 assert_eq!(dom.html_string().await, "<pre>Tack :)</pre>");
169 })
170 .await;
171 });
172 }
173
174 #[test]
175 fn test_append() {
176 let ns = "http://www.w3.org/2000/svg";
177 let _bldr = ViewBuilder::element_ns("svg", ns)
178 .with_single_attrib_stream("width", "100")
179 .with_single_attrib_stream("height", "100")
180 .append(
181 ViewBuilder::element_ns("circle", ns)
182 .with_single_attrib_stream("cx", "50")
183 .with_single_attrib_stream("cy", "50")
184 .with_single_attrib_stream("r", "40")
185 .with_single_attrib_stream("stroke", "green")
186 .with_single_attrib_stream("stroke-width", "4")
187 .with_single_attrib_stream("fill", "yellow"),
188 );
189 }
190
191 #[test]
192 fn input() {
193 let _ = html! {
194 <input boolean:checked=true />
195 };
196 }
197
198 #[test]
199 fn append_works() {
200 let (tx, rx) = broadcast::bounded::<()>(1.try_into().unwrap());
201 let _ = rsx! {
202 div( window:load=tx.contra_map(|_:DomEvent| ()) ) {
203 {("", rx.map(|()| "Loaded!".to_string()))}
204 }
205 };
206 }
207
208 #[test]
209 fn can_append_vec() {
210 let _div: ViewBuilder = ViewBuilder::element("div").append(vec![ViewBuilder::element("p")]);
211 }
212
213 #[test]
214 fn can_append_option() {
215 let _div: ViewBuilder = ViewBuilder::element("div").append(None as Option<ViewBuilder>);
216 }
217
218 #[test]
219 fn fragments() {
220 let vs: Vec<ViewBuilder> = html! {
221 <div>"hello"</div>
222 <div>"hola"</div>
223 <div>"kia ora"</div>
224 };
225
226 let s = html! {
227 <section>{vs}</section>
228 };
229 let view = SsrDom::try_from(s).unwrap();
230 futures::executor::block_on(async move {
231 assert_eq!(
232 view.html_string().await,
233 "<section><div>hello</div> <div>hola</div> <div>kia ora</div></section>"
234 );
235 });
236 }
237
238 #[test]
239 fn post_build_manual() {
240 let (tx, _rx) = broadcast::bounded::<()>(1.try_into().unwrap());
241
242 let _div = ViewBuilder::element("div")
243 .with_single_attrib_stream("id", "hello")
244 .with_post_build(move |_: &mut JsDom| {
245 let _ = tx.inner.try_broadcast(())?;
246 Ok(())
247 })
248 .append(ViewBuilder::text("Hello"));
249 }
250
251 #[test]
252 fn post_build_rsx() {
253 futures::executor::block_on(async {
254 let (tx, mut rx) = broadcast::bounded::<()>(1.try_into().unwrap());
255
256 let _div = SsrDom::try_from(rsx! {
257 div(id="hello", post:build=move |_: &mut SsrDom| {
258 let _ = tx.inner.try_broadcast(())?;
259 Ok(())
260 }) { "Hello" }
261 })
262 .unwrap();
263
264 rx.recv().await.unwrap();
265 });
266 }
267
268 #[test]
269 fn can_construct_text_builder_from_tuple() {
270 futures::executor::block_on(async {
271 let (_tx, rx) = broadcast::bounded::<String>(1.try_into().unwrap());
272 let _div = SsrDom::try_from(html! {
273 <div>{("initial", rx)}</div>
274 })
275 .unwrap();
276 });
277 }
278
279 #[test]
280 fn ssr_properties_overwrite() {
281 let executor = Arc::new(Executor::default());
282 futures::executor::block_on(async {
283 let el: SsrDom = SsrDom::element(executor.clone(), "div");
284 el.set_style("float", "right").unwrap();
285 assert_eq!(
286 el.html_string().await,
287 r#"<div style="float: right;"></div>"#
288 );
289
290 el.set_style("float", "left").unwrap();
291 assert_eq!(
292 el.html_string().await,
293 r#"<div style="float: left;"></div>"#
294 );
295
296 el.set_style("width", "100px").unwrap();
297 assert_eq!(
298 el.html_string().await,
299 r#"<div style="float: left; width: 100px;"></div>"#
300 );
301 });
302 }
303
304 #[test]
305 fn ssr_attrib_overwrite() {
306 let executor = Arc::new(Executor::default());
307 futures::executor::block_on(async {
308 let el: SsrDom = SsrDom::element(executor.clone(), "div");
309
310 el.set_attrib("class", Some("my_class")).unwrap();
311 assert_eq!(el.html_string().await, r#"<div class="my_class"></div>"#);
312
313 el.set_attrib("class", Some("your_class")).unwrap();
314 assert_eq!(el.html_string().await, r#"<div class="your_class"></div>"#);
315 });
316 }
317
318 async fn wait_eq(t: &str, secs: f64, view: &SsrDom) {
319 let start = mogwai::time::now() / 1000.0;
320 let timeout = secs;
321 loop {
322 let s = view.html_string().await;
323 let now = mogwai::time::now() / 1000.0;
324 if (now - start) >= timeout {
325 panic!("timeout {}s: {:?} != {:?} ", timeout, t, s);
326 } else if t.trim() == s.trim() {
327 return;
328 }
329 mogwai_dom::core::time::wait_millis(1).await;
330 }
331 }
332
333 #[test]
334 pub fn ssr_simple_update() {
335 futures_lite::future::block_on(async {
336 let mut text = Input::<String>::default();
337
338 let view =
339 SsrDom::try_from(ViewBuilder::text(("hello", text.stream().unwrap()))).unwrap();
340 let v = view.clone();
341 view.run_while(async move {
342 wait_eq(r#"hello"#, 1.0, &v).await;
343 text.set("goodbye").await.unwrap();
344 wait_eq(r#"goodbye"#, 1.0, &v).await;
345 })
346 .await
347 .unwrap();
348 });
349 }
350
351 #[test]
352 pub fn ssr_simple_nested_update() {
353 futures_lite::future::block_on(async {
354 let mut text = Input::<String>::default();
355
356 let view = SsrDom::try_from(rsx!(
357 p() {
358 {("hello", text.stream().unwrap())}
359 }
360 ))
361 .unwrap();
362 let v = view.clone();
363 view.run_while(async move {
364 wait_eq(r#"<p>hello</p>"#, 1.0, &v).await;
365
366 text.set("goodbye").await.unwrap();
367 wait_eq(r#"<p>goodbye</p>"#, 1.0, &v).await;
368
369 text.set("kia ora").await.unwrap();
370 wait_eq(r#"<p>kia ora</p>"#, 1.0, &v).await;
371 })
372 .await
373 .unwrap();
374 });
375 }
376
377 #[test]
378 pub fn ssr_simple_nested_with_two_inputs_update() {
379 futures_lite::future::block_on(async {
380 let mut text = Input::<String>::default();
381 let mut class = Input::<String>::default();
382
383 let view = SsrDom::try_from(rsx!(
384 p(class=("p_class", class.stream().unwrap())) {
385 {("hello", text.stream().unwrap())}
386 }
387 ))
388 .unwrap();
389 let v = view.clone();
390 view.run_while(async move {
391 wait_eq(r#"<p class="p_class">hello</p>"#, 1.0, &v).await;
392
393 text.set("goodbye").await.unwrap();
394 wait_eq(r#"<p class="p_class">goodbye</p>"#, 1.0, &v).await;
395
396 class.set("my_p_class").await.unwrap();
397 wait_eq(r#"<p class="my_p_class">goodbye</p>"#, 1.0, &v).await;
398 })
399 .await
400 .unwrap();
401 });
402 }
403
404 #[test]
405 pub fn can_alter_ssr_views() {
406 use mogwai::relay::*;
407 futures_lite::future::block_on(async {
408 let mut text = Input::<String>::default();
409 let mut style = Input::<String>::default();
410 let mut class = Input::<String>::default();
411
412 let view = SsrDom::try_from(rsx! {
413 div(style:float=("left", style.stream().unwrap())) {
414 p(class=("p_class", class.stream().unwrap())) {
415 {("here", text.stream().unwrap())}
416 }
417 }
418 })
419 .unwrap();
420
421 let v = view.clone();
422 view.run_while(async move {
423 wait_eq(
424 r#"<div style="float: left;"><p class="p_class">here</p></div>"#,
425 10.0,
426 &v,
427 )
428 .await;
429
430 let _ = text.try_send("there".to_string()).unwrap();
431 wait_eq(
432 r#"<div style="float: left;"><p class="p_class">there</p></div>"#,
433 1.0,
434 &v,
435 )
436 .await;
437
438 let _ = style.try_send("right".to_string()).unwrap();
439 wait_eq(
440 r#"<div style="float: right;"><p class="p_class">there</p></div>"#,
441 1.0,
442 &v,
443 )
444 .await;
445
446 let _ = class.try_send("my_p_class".to_string()).unwrap();
447 wait_eq(
448 r#"<div style="float: right;"><p class="my_p_class">there</p></div>"#,
449 1.0,
450 &v,
451 )
452 .await;
453 })
454 .await
455 .unwrap();
456 });
457 }
458
459 #[test]
460 fn can_use_string_stream_as_child() {
461 futures::executor::block_on(async {
462 let clicks = futures::stream::iter(vec![0, 1, 2]);
463 let bldr = html! {
464 <span>
465 {
466 ViewBuilder::text(clicks.map(|clicks| match clicks {
467 1 => "1 click".to_string(),
468 n => format!("{} clicks", n),
469 }))
470 }
471 </span>
472 };
473 let _ = SsrDom::try_from(bldr).unwrap();
474 });
475 }
476
477 #[test]
478 fn test_use_tx_in_logic_loop() {
479 futures::executor::block_on(async {
480 let executor = Arc::new(Executor::default());
481 let (tx, mut rx) = broadcast::bounded::<()>(1.try_into().unwrap());
482 let (tx_end, mut rx_end) = broadcast::bounded::<()>(1.try_into().unwrap());
483 let tx_logic = tx.clone();
484 executor
485 .spawn(async move {
486 let mut ticks = 0u32;
487 loop {
488 match rx.next().await {
489 Some(()) => {
490 ticks += 1;
491 match ticks {
492 1 => {
493 tx.broadcast(()).await.unwrap();
495 }
496 _ => break,
497 }
498 }
499
500 None => break,
501 }
502 }
503 assert_eq!(ticks, 2);
504 tx_end.broadcast(()).await.unwrap();
505 })
506 .detach();
507 executor
508 .run(async {
509 tx_logic.broadcast(()).await.unwrap();
510 rx_end.next().await.unwrap();
511 })
512 .await;
513 });
514 }
515
516 #[test]
517 fn patch_children_rsx_md() {
518 futures::executor::block_on(async {
519 let (tx, rx) = mpsc::bounded(1);
521 let my_view = SsrDom::try_from(html! {
522 <div id="main" patch:children=rx>"Waiting for a patch message..."</div>
523 })
524 .unwrap();
525
526 my_view
527 .executor
528 .run(async {
529 tx.send(ListPatch::drain()).await.unwrap();
530 repeat_times(0.1, 10, || async {
533 my_view.html_string().await == r#"<div id="main"></div>"#
534 })
535 .await
536 .unwrap();
537
538 let other_viewbuilder = html! {
539 <h1>"Hello!"</h1>
540 };
541
542 tx.send(ListPatch::push(other_viewbuilder)).await.unwrap();
543 repeat_times(0.1, 10, || async {
545 let html_string = my_view.html_string().await;
546 html_string == r#"<div id="main"><h1>Hello!</h1></div>"#
547 })
548 .await
549 .unwrap();
550 })
551 .await;
552 });
554 }
555
556 #[test]
557 pub fn can_build_readme_button() {}
558}
559
560#[cfg(all(test, target_arch = "wasm32"))]
561mod wasm {
562 use std::ops::Bound;
563
564 use crate as mogwai_dom;
565 use crate::{
566 core::{
567 channel::{broadcast, mpsc},
568 time::*,
569 },
570 prelude::*,
571 view::js::Hydrator,
572 };
573 use mogwai::{stream, model::Model};
575 use wasm_bindgen::JsCast;
576 use wasm_bindgen_test::*;
577 use web_sys::HtmlElement;
578
579 wasm_bindgen_test_configure!(run_in_browser);
580
581 #[wasm_bindgen_test]
582 async fn can_create_text_view_node_from_str() {
583 let _view: JsDom = ViewBuilder::text("Hello!").try_into().unwrap();
584 }
585
586 #[wasm_bindgen_test]
587 async fn can_create_text_view_node_from_string() {
588 let _view: JsDom = ViewBuilder::text("Hello!".to_string()).try_into().unwrap();
589 }
590
591 #[wasm_bindgen_test]
592 async fn can_create_text_view_node_from_stream() {
593 let s = stream::once("Hello!".to_string());
594 let _view: JsDom = ViewBuilder::text(s).try_into().unwrap();
595 }
596
597 #[wasm_bindgen_test]
598 async fn can_create_text_view_node_from_string_and_stream() {
599 let s = "Hello!".to_string();
600 let st = stream::once("Goodbye!".to_string());
601 let _view: JsDom = ViewBuilder::text((s, st)).try_into().unwrap();
602 }
603
604 #[wasm_bindgen_test]
605 async fn can_create_text_view_node_from_str_and_stream() {
606 let st = stream::once("Goodbye!".to_string());
607 let _view: JsDom = ViewBuilder::text(("Hello!", st)).try_into().unwrap();
608 }
609
610 #[wasm_bindgen_test]
611 async fn can_nest_created_text_view_node() {
612 let view: JsDom = ViewBuilder::element("div")
613 .append(ViewBuilder::text("Hello!"))
614 .with_single_attrib_stream("id", "view1")
615 .with_single_style_stream("color", "red")
616 .with_single_style_stream("width", "100px")
617 .try_into()
618 .unwrap();
619
620 assert_eq!(
621 r#"<div id="view1" style="color: red; width: 100px;">Hello!</div>"#,
622 view.html_string().await,
623 );
624 }
625
626 #[wasm_bindgen_test]
627 async fn ssr_can_nest_created_text_view_node() {
628 let view: JsDom = ViewBuilder::element("div")
629 .append(ViewBuilder::text("Hello!"))
630 .with_single_attrib_stream("id", "view1")
631 .with_single_style_stream("color", "red")
632 .with_single_style_stream("width", "100px")
633 .try_into()
634 .unwrap();
635
636 assert_eq!(
637 view.html_string().await,
638 r#"<div id="view1" style="color: red; width: 100px;">Hello!</div>"#
639 );
640 }
641
642 #[wasm_bindgen_test]
643 async fn can_use_rsx_to_make_builder() {
644 let (tx, _) = broadcast::bounded::<AnyEvent>(1);
645
646 let rsx = html! {
647 <div id="view_zero" style:background_color="red">
648 <pre on:click=tx.clone()>"this has text"</pre>
649 </div>
650 };
651 let rsx_view: JsDom = rsx.try_into().unwrap();
652
653 let manual = ViewBuilder::element("div")
654 .with_single_attrib_stream("id", "view_zero")
655 .with_single_style_stream("background-color", "red")
656 .append(
657 ViewBuilder::element("pre")
658 .with_event("click", "myself", tx)
659 .append(ViewBuilder::text("this has text")),
660 );
661 let manual_view: JsDom = manual.try_into().unwrap();
662
663 assert_eq!(
664 rsx_view.html_string().await,
665 manual_view.html_string().await
666 );
667 }
668
669 #[wasm_bindgen_test]
670 async fn viewbuilder_child_order() {
671 let v: JsDom = html! {
672 <div>
673 <p id="one">"i am 1"</p>
674 <p id="two">"i am 2"</p>
675 <p id="three">"i am 3"</p>
676 </div>
677 }
678 .try_into()
679 .unwrap();
680
681 let nodes = v.dyn_ref::<web_sys::Node>().unwrap().child_nodes();
682 let len = nodes.length();
683 assert_eq!(len, 3);
684 let mut ids = vec![];
685 for i in 0..len {
686 let el = nodes
687 .get(i)
688 .unwrap()
689 .dyn_into::<web_sys::Element>()
690 .unwrap();
691 ids.push(el.id());
692 }
693
694 assert_eq!(ids.as_slice(), ["one", "two", "three"]);
695 }
696
697 #[wasm_bindgen_test]
698 async fn gizmo_as_child() {
699 let div = {
702 let div: JsDom = html! {
703 <div id="parent-div">
704 <pre>"some text"</pre>
705 </div>
706 }
707 .try_into()
708 .unwrap();
709 assert!(
710 div.clone_as::<web_sys::HtmlElement>()
711 .unwrap()
712 .first_child()
713 .is_some(),
714 "could not add child gizmo"
715 );
716 div
717 };
718 assert!(
719 div.clone_as::<web_sys::HtmlElement>()
720 .unwrap()
721 .first_child()
722 .is_some(),
723 "could not keep hold of child gizmo"
724 );
725 assert_eq!(
726 div.clone_as::<web_sys::HtmlElement>()
727 .unwrap()
728 .child_nodes()
729 .length(),
730 1,
731 "parent is missing static_gizmo"
732 );
733 }
734
735 #[wasm_bindgen_test]
736 async fn gizmo_tree() {
737 let root: JsDom = html! {
738 <div id="root">
739 <div id="branch">
740 <div id="leaf">
741 "leaf"
742 </div>
743 </div>
744 </div>
745 }
746 .try_into()
747 .unwrap();
748 let el = root.clone_as::<web_sys::HtmlElement>().unwrap();
749 if let Some(branch) = el.first_child() {
750 if let Some(leaf) = branch.first_child() {
751 if let Some(leaf) = leaf.dyn_ref::<web_sys::Element>() {
752 assert_eq!(leaf.id(), "leaf");
753 } else {
754 panic!("leaf is not an Element");
755 }
756 } else {
757 panic!("branch has no leaf");
758 }
759 } else {
760 panic!("root has no branch");
761 }
762 }
763
764 #[wasm_bindgen_test]
765 async fn gizmo_texts() {
766 let div: JsDom = rsx! {
767 div() {
768 "here is some text "
769 {format!("{}", 66)}
771 " <- number"
772 }
773 }
774 .try_into()
775 .unwrap();
776 assert_eq!(
777 &div.clone_as::<web_sys::Element>().unwrap().outer_html(),
778 "<div>here is some text 66 <- number</div>"
779 );
780 }
781
782 #[wasm_bindgen_test]
783 async fn rx_attribute_jsx() {
784 let (tx, rx) = broadcast::bounded::<String>(1);
785 let div: JsDom = html! {
786 <div class=("now", rx) />
787 }
788 .try_into()
789 .unwrap();
790 let div_el: web_sys::HtmlElement = div.clone_as::<web_sys::HtmlElement>().unwrap();
791 assert_eq!(div_el.outer_html(), r#"<div class="now"></div>"#);
792
793 tx.broadcast("later".to_string()).await.unwrap();
794 tx.until_empty().await;
795
796 assert_eq!(div_el.outer_html(), r#"<div class="later"></div>"#);
797 }
798
799 #[wasm_bindgen_test]
800 async fn rx_style_jsx() {
801 let (tx, rx) = broadcast::bounded::<String>(1);
802 let div: JsDom = html! { <div style:display=("block", rx) /> }
803 .try_into()
804 .unwrap();
805 let div_el = div.clone_as::<web_sys::HtmlElement>().unwrap();
806 assert_eq!(
807 div_el.outer_html(),
808 r#"<div style="display: block;"></div>"#
809 );
810
811 tx.broadcast("none".to_string()).await.unwrap();
812 tx.until_empty().await;
813
814 assert_eq!(div_el.outer_html(), r#"<div style="display: none;"></div>"#);
815 }
816
817 #[wasm_bindgen_test]
818 async fn capture_view_and_contra_map() {
819 let (tx, mut rx) = broadcast::bounded::<()>(1);
820 let _div: JsDom = html! {
821 <div id="hello" capture:view=tx.contra_map(|_: JsDom| ())>
822 "Hello there"
823 </div>
824 }
825 .try_into()
826 .unwrap();
827
828 let () = rx.recv().await.unwrap();
829 }
830
831 #[wasm_bindgen_test]
832 pub async fn initial_text() {
833 let dom = JsDom::try_from(ViewBuilder::text((
834 "hello",
835 stream::once("goodbye".to_string()),
836 )))
837 .unwrap();
838 assert_eq!("hello", dom.html_string().await);
839 }
840
841 #[wasm_bindgen_test]
842 pub async fn initial_text_nested() {
843 let dom = JsDom::try_from(ViewBuilder::element("div").append(ViewBuilder::text((
844 "hello",
845 stream::once("goodbye".to_string()),
846 ))))
847 .unwrap();
848 assert_eq!("<div>hello</div>", dom.html_string().await);
849 }
850
851 #[wasm_bindgen_test]
852 pub async fn rx_text() {
853 let (tx, rx) = broadcast::bounded::<String>(1);
854
855 let div: JsDom = html! {
856 <div>{("initial", rx)}</div>
857 }
858 .try_into()
859 .unwrap();
860
861 let el = div.clone_as::<web_sys::HtmlElement>().unwrap();
862 assert_eq!(el.inner_text().as_str(), "initial");
863
864 tx.broadcast("after".into()).await.unwrap();
865 tx.until_empty().await;
866
867 assert_eq!(el.inner_text(), "after");
868 }
869
870 #[wasm_bindgen_test]
871 async fn tx_on_click() {
872 let (tx, rx) = broadcast::bounded(1);
873
874 log::info!("test!");
875 let rx = rx.scan(0, |n: &mut i32, _: JsDomEvent| {
876 log::info!("event!");
877 *n += 1;
878 Some(if *n == 1 {
879 "Clicked 1 time".to_string()
880 } else {
881 format!("Clicked {} times", *n)
882 })
883 });
884
885 let button: JsDom = html! {
886 <button on:click=tx.clone()>{("Clicked 0 times", rx)}</button>
887 }
888 .try_into()
889 .unwrap();
890
891 let el = button.clone_as::<web_sys::HtmlElement>().unwrap();
892 assert_eq!(el.inner_html(), "Clicked 0 times");
893
894 el.click();
895 tx.until_empty().await;
896 let _ = wait_millis(1000).await;
897
898 assert_eq!(el.inner_html(), "Clicked 1 time");
899 }
900
901 #[wasm_bindgen_test]
908 async fn can_patch_children() {
909 let (tx, rx) = mpsc::bounded::<ListPatch<ViewBuilder>>(1);
910 let view: JsDom = html! {
911 <ol id="main" patch:children=rx>
912 <li>"Zero"</li>
913 <li>"One"</li>
914 </ol>
915 }
916 .try_into()
917 .unwrap();
918
919 let dom: HtmlElement = view.clone_as::<HtmlElement>().unwrap();
920 view.run().unwrap();
921
922 wait_while(1.0, || {
923 dom.outer_html().as_str() != r#"<ol id="main"><li>Zero</li><li>One</li></ol>"#
924 })
925 .await
926 .unwrap();
927
928 let html = r#"<ol id="main"><li>Zero</li><li>One</li><li>Two</li></ol>"#;
929 tx.send(ListPatch::push(html! {<li>"Two"</li>}))
930 .await
931 .unwrap();
932 let _ = wait_while(5.0, || dom.outer_html().as_str() != html).await;
933 assert_eq!(html, dom.outer_html());
934
935 tx.send(ListPatch::splice(0..1, None.into_iter()))
936 .await
937 .unwrap();
938 wait_while(1.0, || {
939 dom.outer_html().as_str() != r#"<ol id="main"><li>One</li><li>Two</li></ol>"#
940 })
941 .await
942 .unwrap();
943
944 tx.send(ListPatch::splice(
945 0..0,
946 Some(html! {<li>"Zero"</li>}).into_iter(),
947 ))
948 .await
949 .unwrap();
950 wait_while(1.0, || {
951 dom.outer_html().as_str()
952 != r#"<ol id="main"><li>Zero</li><li>One</li><li>Two</li></ol>"#
953 })
954 .await
955 .unwrap();
956
957 tx.send(ListPatch::splice(2..3, None.into_iter()))
958 .await
959 .unwrap();
960 wait_while(1.0, || {
961 dom.outer_html().as_str() != r#"<ol id="main"><li>Zero</li><li>One</li></ol>"#
962 })
963 .await
964 .unwrap();
965
966 tx.send(ListPatch::splice(
967 0..0,
968 Some(html! {<li>"Negative One"</li>}).into_iter(),
969 ))
970 .await
971 .unwrap();
972 wait_while(1.0, || {
973 dom.outer_html().as_str()
974 != r#"<ol id="main"><li>Negative One</li><li>Zero</li><li>One</li></ol>"#
975 })
976 .await
977 .unwrap();
978
979 tx.send(ListPatch::Pop).await.unwrap();
980 wait_while(1.0, || {
981 dom.outer_html().as_str() != r#"<ol id="main"><li>Negative One</li><li>Zero</li></ol>"#
982 })
983 .await
984 .unwrap();
985
986 tx.send(ListPatch::splice(
987 1..2,
988 Some(html! {<li>"One"</li>}).into_iter(),
989 ))
990 .await
991 .unwrap();
992 wait_while(1.0, || {
993 dom.outer_html().as_str() != r#"<ol id="main"><li>Negative One</li><li>One</li></ol>"#
994 })
995 .await
996 .unwrap();
997
998 use std::ops::RangeBounds;
999 let range = 0..;
1000 let (start, end) = (range.start_bound(), range.end_bound());
1001 assert_eq!(start, Bound::Included(&0));
1002 assert_eq!(end, Bound::Unbounded);
1003 assert!((start, end).contains(&1));
1004
1005 tx.send(ListPatch::splice(0.., None.into_iter()))
1006 .await
1007 .unwrap();
1008 wait_while(1.0, || {
1009 dom.outer_html().as_str() != r#"<ol id="main"></ol>"#
1010 })
1011 .await
1012 .unwrap();
1013 }
1014
1015 #[wasm_bindgen_test]
1016 async fn can_patch_children_into() {
1017 let (tx, rx) = mpsc::bounded::<ListPatch<String>>(1);
1018 let view: JsDom = html! {
1019 <p id="main" patch:children=rx.map(|p| p.map(|s| ViewBuilder::text(s)))>
1020 "Zero ""One"
1021 </p>
1022 }
1023 .try_into()
1024 .unwrap();
1025
1026 let dom: HtmlElement = view.clone_as().unwrap();
1027 view.run().unwrap();
1028
1029 assert_eq!(dom.outer_html().as_str(), r#"<p id="main">Zero One</p>"#);
1030
1031 tx.send(ListPatch::splice(
1032 0..0,
1033 std::iter::once("First ".to_string()),
1034 ))
1035 .await
1036 .unwrap();
1037 wait_while(1.0, || {
1038 dom.outer_html().as_str() != r#"<p id="main">First Zero One</p>"#
1039 })
1040 .await
1041 .unwrap();
1042
1043 tx.send(ListPatch::splice(.., std::iter::empty()))
1044 .await
1045 .unwrap();
1046 wait_while(1.0, || dom.outer_html().as_str() != r#"<p id="main"></p>"#)
1047 .await
1048 .unwrap();
1049 }
1050
1051 #[wasm_bindgen_test]
1052 pub async fn can_use_string_stream_as_child() {
1053 let clicks = futures::stream::iter(vec![0, 1, 2]);
1054 let bldr = html! {
1055 <span>
1056 {
1057 ViewBuilder::text(clicks.map(|clicks| match clicks {
1058 1 => "1 click".to_string(),
1059 n => format!("{} clicks", n),
1060 }))
1061 }
1062 </span>
1063 };
1064 let _: JsDom = bldr.try_into().unwrap();
1065 }
1066
1067 fn sendable<T: Send + Sync + 'static>(_: &T) {}
1068
1069 #[wasm_bindgen_test]
1070 pub fn output_sendable() {
1071 let output: Output<JsDom> = Output::default();
1072 sendable(&output);
1073
1074 wasm_bindgen_futures::spawn_local(async move {
1075 let _ = output;
1076 })
1077 }
1078
1079 #[wasm_bindgen_test]
1080 async fn can_capture_with_captured() {
1081 let capture: Captured<JsDom> = Captured::default().clone();
1082 let b = html! {
1083 <div id="chappie" capture:view=capture.sink()></div>
1084 };
1085 let _: JsDom = b.try_into().unwrap();
1086 let dom = capture.get().await;
1087 assert_eq!(dom.html_string().await, r#"<div id="chappie"></div>"#);
1088 }
1089
1090 #[wasm_bindgen_test]
1091 async fn can_hydrate_view() {
1092 console_log::init_with_level(log::Level::Trace).unwrap();
1093
1094 let container = JsDom::try_from(html! {
1095 <div id="hydrator1"></div>
1096 })
1097 .unwrap();
1098 let container_el: HtmlElement = container.clone_as::<HtmlElement>().unwrap();
1099 container.run().unwrap();
1100 container_el.set_inner_html(r#"<div id="my_div"><p>inner text</p></div>"#);
1101 assert_eq!(
1102 container_el.inner_html().as_str(),
1103 r#"<div id="my_div"><p>inner text</p></div>"#
1104 );
1105 log::info!("built");
1106
1107 let (tx_class, rx_class) = mpsc::bounded::<String>(1);
1108 let (tx_text, rx_text) = mpsc::bounded::<String>(1);
1109 let builder = html! {
1110 <div id="my_div">
1111 <p class=rx_class>{("", rx_text)}</p>
1112 </div>
1113 };
1114 let hydrator = Hydrator::try_from(builder)
1115 .map_err(|e| panic!("{:#?}", e))
1116 .unwrap();
1117 let _hydrated_view: JsDom = JsDom::from(hydrator);
1118 log::info!("hydrated");
1119
1120 tx_class.send("new_class".to_string()).await.unwrap();
1121 repeat_times(0.1, 10, || async {
1122 container_el.inner_html().as_str()
1123 == r#"<div id="my_div"><p class="new_class">inner text</p></div>"#
1124 })
1125 .await
1126 .unwrap();
1127 log::info!("updated class");
1128
1129 tx_text
1130 .send("different inner text".to_string())
1131 .await
1132 .unwrap();
1133 repeat_times(0.1, 10, || async {
1134 container_el.inner_html().as_str()
1135 == r#"<div id="my_div"><p class="new_class">different inner text</p></div>"#
1136 })
1137 .await
1138 .unwrap();
1139 log::info!("updated text");
1140 }
1141
1142 #[wasm_bindgen_test]
1143 async fn can_capture_for_each() {
1144 let (tx, rx) = mpsc::bounded(1);
1145 let (tx_done, mut rx_done) = mpsc::bounded(1);
1146 let dom = JsDom::try_from(rsx! {
1147 input(
1148 type = "text",
1149 capture:for_each = (
1150 rx.map(|n:usize| format!("{}", n)),
1151 JsDom::try_to(web_sys::HtmlInputElement::set_value)
1152 )
1153 ) {}
1154 })
1155 .expect("could not build dom");
1156
1157 wasm_bindgen_futures::spawn_local(async move {
1158 let mut n = 0;
1159 while n < 3 {
1160 tx.send(n).await.unwrap();
1161 n += 1;
1162 }
1163 tx_done.send(()).await.unwrap();
1164 });
1165
1166 dom.run_while(async move {
1167 let _ = rx_done.next().await;
1168 })
1169 .await
1170 .unwrap();
1171
1172 let value = dom
1173 .visit_as(|input: &web_sys::HtmlInputElement| input.value())
1174 .unwrap();
1175 assert_eq!("2", value.as_str());
1176 }
1177
1178 #[wasm_bindgen_test]
1179 async fn can_hydrate_and_update() {
1180 fn builder(
1181 id: impl Stream<Item = usize> + Send + Sync + 'static,
1182 label: impl Stream<Item = String> + Send + Sync + 'static,
1183 root: Option<&JsDom>,
1184 ) -> ViewBuilder {
1185 let builder = rsx!(
1186 div(id = ("0", id.map(|i| format!("{}", i)))) {
1187 {("unknown", label)}
1188 }
1189 );
1190 if let Some(root) = root {
1191 builder.with_hydration_root(JsDom::from_jscast(
1192 &root
1193 .visit_as::<web_sys::Node, web_sys::Node>(|n| {
1194 n.clone_node_with_deep(true).unwrap()
1195 })
1196 .unwrap(),
1197 ))
1198 } else {
1199 builder
1200 }
1201 }
1202
1203 let root = JsDom::try_from(builder(
1204 mogwai::stream::once(0),
1205 mogwai::stream::once("yar".to_string()),
1206 None,
1207 ))
1208 .unwrap()
1209 .ossify();
1210 assert_eq!(r#"<div id="0">unknown</div>"#, root.html_string().await);
1211
1212 let mut id = Input::<usize>::default();
1213 let mut label = Input::<String>::default();
1214 let hydrated = JsDom::try_from(builder(
1215 id.stream().unwrap(),
1216 label.stream().unwrap(),
1217 Some(&root),
1218 ))
1219 .unwrap();
1220 id.set(1usize).await.unwrap();
1221 label.set("hello").await.unwrap();
1222 mogwai::time::wait_one_frame().await;
1223 assert_eq!(r#"<div id="1">hello</div>"#, hydrated.html_string().await);
1224 }
1225
1226 #[wasm_bindgen_test]
1227 async fn can_nest_jsdom_as_viewbuilder() {
1228 let model = Model::<(usize, String)>::new((666, "hello".into()));
1229 let child = JsDom::try_from(rsx! {
1230 p(id = model.clone().map(|m| m.0.to_string())){{ model.clone().map(|m| m.1) }}
1231 }).unwrap();
1232 let parent = JsDom::try_from(rsx! {
1233 div(){{ child }}
1234 }).unwrap();
1235 assert_eq!(r#"<div><p id="666">hello</p></div>"#, parent.html_string().await);
1236
1237 model.replace((123, "goodbye".into())).await;
1238 mogwai::time::wait_millis(10).await;
1239 assert_eq!(r#"<div><p id="123">goodbye</p></div>"#, parent.html_string().await);
1240 }
1241
1242 #[wasm_bindgen_test]
1243 async fn can_use_dom() {
1244 use mogwai::time;
1245
1246 let click = Output::<()>::default();
1247 let mut text = Input::<String>::default();
1248 let view = Dom::try_from({
1249 let builder = rsx! {
1250 div() {
1251 button(
1252 id = "button",
1253 on:click = click.sink().contra_map(|_:DomEvent| ()),
1254 ) {
1255 "click me"
1256 }
1257 span(){
1258 {("", text.stream().unwrap())}
1259 }
1260 }
1261 };
1262 builder.with_task(async move {
1263 let mut t = 0;
1264 while let Some(()) = click.get().await {
1265 t += 1;
1266 text.set(format!("{}", t)).await.unwrap();
1267 }
1268 })
1269 }).unwrap();
1270 let div = view.clone();
1271 view.run().unwrap();
1272
1273 div.as_either_ref().left().unwrap().visit_as(|el: &web_sys::HtmlElement| {
1274 let button = el
1275 .query_selector("#button")
1276 .unwrap()
1277 .unwrap()
1278 .dyn_into::<web_sys::HtmlElement>()
1279 .unwrap();
1280 button.click();
1281 });
1282 time::wait_millis(10).await;
1283 }
1284}
1285
1286#[cfg(test)]
1287mod test {
1288
1289 use crate as mogwai_dom;
1290 use crate::prelude::*;
1291
1292 #[test]
1293 fn can_relay() {
1294 struct Thing {
1295 view: Output<Dom>,
1296 click: Output<()>,
1297 text: Input<String>,
1298 }
1299
1300 impl Default for Thing {
1301 fn default() -> Self {
1302 Self {
1303 view: Default::default(),
1304 click: Default::default(),
1305 text: Default::default(),
1306 }
1307 }
1308 }
1309
1310 impl Thing {
1311 fn view(mut self) -> ViewBuilder {
1312 rsx! (
1313 div(
1314 capture:view=self.view.sink(),
1315 on:click=self.click.sink().contra_map(|_: AnyEvent| ())
1316 ) {
1317 {("Hi", self.text.stream().unwrap())}
1318 }
1319 )
1320 .with_task(async move {
1321 let mut clicks = 0;
1322 while let Some(()) = self.click.get().await {
1323 clicks += 1;
1324 self.text
1325 .set(if clicks == 1 {
1326 "1 click.".to_string()
1327 } else {
1328 format!("{} clicks.", clicks)
1329 })
1330 .await
1331 .unwrap_or_else(|_| panic!("could not set text"));
1332 }
1333 })
1334 }
1335 }
1336
1337 let thing: Dom = Dom::try_from(Thing::default().view()).unwrap();
1338 futures::executor::block_on(async move {
1339 thing
1340 .run_while(async {
1341 let _ = crate::core::time::wait_millis(10).await;
1342 })
1343 .await
1344 .unwrap();
1345 });
1346 }
1347
1348 #[test]
1349 fn can_capture_with_captured() {
1350 futures::executor::block_on(async move {
1351 let capture: Captured<SsrDom> = Captured::default();
1352 let b = rsx! {
1353 div(id="chappie", capture:view=capture.sink()){}
1354 };
1355 let dom = SsrDom::try_from(b).unwrap();
1356 dom.executor
1357 .run(async {
1358 let dom = capture.get().await;
1359 assert_eq!(dom.html_string().await, r#"<div id="chappie"></div>"#);
1360 })
1361 .await;
1362 });
1363 }
1364
1365 #[test]
1366 fn how_to_set_properties() {
1367 let mut stream_input_value = Input::<String>::default();
1368 let _builder = rsx! {
1369 input(
1370 type = "text",
1371 id = "my_text",
1372 capture:for_each = (
1373 stream_input_value.stream().unwrap(),
1374 JsDom::try_to(web_sys::HtmlInputElement::set_value)
1375 )
1376 ){}
1377 };
1378 }
1379}