mogwai_dom/
lib.rs

1//! # Mogwai
2//!
3//! Mogwai is library for multi-domain user interface development using sinks
4//! and streams.
5//!
6//! Its goals are simple:
7//! * provide a declarative approach to creating and managing interface nodes,
8//!   without a bias towards a specific UI domain (ie web, games, desktop
9//!   applications, mobile)
10//! * encapsulate component state and compose components easily
11//! * explicate mutations and updates
12//! * feel snappy
13//!
14//! ## Javascript/Browser DOM
15//! This library is specific to writing mogwai apps to run in the browser via
16//! WASM.
17//!
18//! ## Learn more
19//! Please check out the [introduction module](an_introduction).
20//!
21//! ## Acronyms
22//! If you're wondering what the acronym "mogwai" stands for, here is a table of
23//! options that work well, depending on the domain. It's fun to mix and match.
24//!
25//! | M           | O         | G           | W      | A             | I            |
26//! |-------------|-----------|-------------|--------|---------------|--------------|
27//! | minimal     | obvious   | graphical   | web    | application   | interface    |
28//! | modular     | operable  | graphable   | widget |               |              |
29//! | mostly      |           | gui         | work   |               |              |
30//!
31//! ## JavaScript interoperability
32//! This library is a thin layer on top of the [web-sys](https://crates.io/crates/web-sys)
33//! crate which provides raw bindings to _tons_ of browser web APIs.
34//! Many of the DOM specific structs, enums and traits come from `web-sys`.
35//! It is important to understand the [`JsCast`](../prelude/trait.JsCast.html)
36//! trait for writing web apps in Rust. Specifically its `dyn_into` and
37//! `dyn_ref` functions are the primary way to cast JavaScript values as
38//! specific Javascript types.
39pub mod an_introduction;
40pub mod event;
41pub mod utils;
42pub mod view;
43pub use mogwai_macros::{builder, html, rsx};
44
45pub mod core {
46    //! Re-export of the mogwai library.
47    pub use mogwai::*;
48}
49
50pub mod prelude {
51    //! Re-exports for convenience.
52    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    }; // for macro features
74
75    #[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        // ANCHOR: capture_view_channel_md
104        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        // ANCHOR_END: capture_view_channel_md
125    }
126
127    #[test]
128    fn capture_view_captured_md() {
129        // ANCHOR_END: capture_view_captured_md
130        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        // ANCHOR_END: capture_view_captured_md
149    }
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                                        // while in the loop, queue another
494                                        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            // ANCHOR: patch_children_rsx
520            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                    // just as a sanity check we wait until the view has removed all child
531                    // nodes
532                    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                    // now wait until the view has been patched with the new child
544                    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            // ANCHOR_END:patch_children_rsx
553        });
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 futures::stream;
574    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        // Since the pre tag is *not* dropped after the scope block the last assert
700        // should show that the div tag has a child.
701        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                // i can use comments, yay!
770                {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 &lt;- 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    //fn nice_compiler_error() {
902    //    let _div = html! {
903    //        <div unknown:colon:thing="not ok" />
904    //    };
905    //}
906
907    #[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}