workflow_html/
lib.rs

1//!
2//! [<img alt="github" src="https://img.shields.io/badge/github-workflow--rs-8da0cb?style=for-the-badge&labelColor=555555&color=8da0cb&logo=github" height="20">](https://github.com/workflow-rs/workflow-rs)
3//! [<img alt="crates.io" src="https://img.shields.io/crates/v/workflow-html.svg?maxAge=2592000&style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/workflow-html)
4//! [<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-workflow--html-56c2a5?maxAge=2592000&style=for-the-badge&logo=docs.rs" height="20">](https://docs.rs/workflow-html)
5//! <img alt="license" src="https://img.shields.io/crates/l/workflow-html.svg?maxAge=2592000&color=6ac&style=for-the-badge&logoColor=fff" height="20">
6//! <img src="https://img.shields.io/badge/platform- wasm32/browser -informational?style=for-the-badge&color=50a0f0" height="20">
7//!
8//! [`workflow-html`](self) crate provides HTML templating macros that return
9//! an [`Html`] structure containing a collection of DOM elements as well as retained
10//! Rust structures supplied to the template. This ensures the lifetime of Rust
11//! structures for the period [`Html`] structure is kept alive. Dropping [`Html`]
12//! structure destroys all retained DOM elements as well as Rust structures.
13//!
14//! By retaining Rust structures this API ensures that elements and callbacks
15//! created by Rust-based HTML elements are retained for the duration of the
16//! Html litefime.
17//!
18//! In addition, HTML elements marked with `@name` attributes are collected into
19//! a separate `HashMap` allowing client to side-access them for external bindings.
20//!
21//! This crate works in conjunction with [`workflow-ux`](https://crates.io/crates/workflow-ux)
22//! allowing Rust HTML Form binding to HTML.
23//!
24//!
25
26pub mod escape;
27pub mod interface;
28pub mod render;
29pub mod utils;
30pub use interface::{Hooks, Html};
31
32pub use escape::{escape_attr, escape_html};
33pub use render::{Render, Renderables, Result, Write};
34use std::collections::BTreeMap;
35use std::sync::{Arc, Mutex};
36pub use utils::{document, Element as WebElement, ElementResult};
37use wasm_bindgen::prelude::*;
38use wasm_bindgen::JsCast;
39pub use workflow_html_macros::{html, html_str, renderable, tree};
40
41#[derive(Debug, Clone)]
42pub enum AttributeValue {
43    Bool(bool),
44    Str(String),
45}
46
47pub type OnClickClosure = Closure<dyn FnMut(web_sys::MouseEvent)>;
48
49#[derive(Debug, Default, Clone)]
50pub struct Element<T: Render> {
51    pub is_fragment: bool,
52    pub tag: String,
53    pub attributes: BTreeMap<String, AttributeValue>,
54    pub children: Option<T>,
55    pub reff: Option<(String, String)>,
56    pub onclick: Arc<Mutex<Option<OnClickClosure>>>,
57}
58
59impl<T: Render + Clone + 'static> Element<T> {
60    pub fn on(self, name: &str, cb: Box<dyn Fn(web_sys::MouseEvent, WebElement)>) -> Self {
61        if name.eq("click") {
62            let mut onclick = self.onclick.lock().unwrap();
63            *onclick = Some(Closure::<dyn FnMut(web_sys::MouseEvent)>::new(Box::new(
64                move |event: web_sys::MouseEvent| {
65                    let target = event.target().unwrap().dyn_into::<WebElement>().unwrap();
66                    cb(event, target)
67                },
68            )));
69        }
70        self
71    }
72    //self_.home_item.element.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())?;
73}
74
75pub trait ElementDefaults {
76    fn _get_attributes(&self) -> String;
77    fn _get_children(&self) -> String;
78
79    fn get_attributes(&self) -> String {
80        self._get_attributes()
81    }
82    fn get_children(&self) -> String {
83        self._get_children()
84    }
85}
86
87impl<T: Render + Clone + 'static> Render for Element<T> {
88    fn render_node(
89        self,
90        parent: &mut WebElement,
91        map: &mut Hooks,
92        renderables: &mut Renderables,
93    ) -> ElementResult<()> {
94        renderables.push(Arc::new(self.clone()));
95        let mut el = document().create_element(&self.tag)?;
96
97        let onclick = self.onclick.lock().unwrap();
98        if let Some(onclick) = onclick.as_ref() {
99            el.add_event_listener_with_callback("click", onclick.as_ref().unchecked_ref())?;
100        }
101
102        for (key, value) in &self.attributes {
103            match value {
104                AttributeValue::Bool(v) => {
105                    if *v {
106                        el.set_attribute(key, "true")?;
107                    }
108                }
109                AttributeValue::Str(v) => {
110                    el.set_attribute(key, &escape_attr(v))?;
111                }
112            }
113        }
114        if let Some((key, value)) = self.reff {
115            el.set_attribute("data-ref", &value)?;
116            map.insert(key, el.clone());
117        }
118        if let Some(children) = self.children {
119            children.render_node(&mut el, map, renderables)?;
120        }
121
122        parent.append_child(&el)?;
123        Ok(())
124    }
125    fn render(&self, w: &mut Vec<String>) -> ElementResult<()> {
126        if self.is_fragment {
127            if let Some(children) = &self.children {
128                children.render(w)?;
129            }
130        } else {
131            w.push(format!("<{}", self.tag));
132            for (key, value) in &self.attributes {
133                match value {
134                    AttributeValue::Bool(v) => {
135                        if *v {
136                            w.push(format!(" {key}"));
137                        }
138                    }
139                    AttributeValue::Str(v) => {
140                        w.push(format!(" {}=\"{}\"", key, escape_attr(v)));
141                    }
142                }
143            }
144            w.push(">".to_string());
145
146            if let Some(children) = &self.children {
147                children.render(w)?;
148            }
149            w.push(format!("</{}>", self.tag));
150        }
151        Ok(())
152    }
153
154    fn remove_event_listeners(&self) -> ElementResult<()> {
155        *self.onclick.lock().unwrap() = None;
156        if let Some(children) = &self.children {
157            children.remove_event_listeners()?;
158        }
159        Ok(())
160    }
161}
162
163#[cfg(test)]
164mod test {
165    //cargo test -- --nocapture --test-threads=1
166    use crate as workflow_html;
167    use crate::*;
168    #[test]
169    pub fn simple_html() {
170        self::print_hr("simple_html");
171        let active = "true";
172        let tree = tree! {
173            <p>
174                <div class="xyz abc active" active>{"some inner html"}</div>
175                <div class={"abc"}>"xyz"</div>
176            </p>
177        };
178        let result = tree.html();
179        println!("html: {}", result);
180        assert_eq!(
181            result,
182            "<p><div active=\"true\" class=\"xyz abc active\">some inner html</div><div class=\"abc\">xyz</div></p>"
183        );
184    }
185    #[test]
186    pub fn custom_elements() {
187        self::print_hr("simple_html");
188        let tree = tree! {
189            <flow-select>
190                <flow-menu-item class="xyz" />
191                <flow-menu-item class={"abc"} />
192            </flow-select>
193        };
194        let result = tree.html();
195        println!("html: {}", result);
196        assert_eq!(result, "<flow-select><flow-menu-item class=\"xyz\"></flow-menu-item><flow-menu-item class=\"abc\"></flow-menu-item></flow-select>");
197    }
198    #[test]
199    pub fn without_root_element() {
200        self::print_hr("without_root_element");
201        let tree = tree! {
202            <div class="xyz"></div>
203            <div class={"abc"}></div>
204        };
205        let result = tree.html();
206        println!("html: {}", result);
207        assert_eq!(result, "<div class=\"xyz\"></div><div class=\"abc\"></div>");
208    }
209    #[test]
210    pub fn complex_html() {
211        self::print_hr("complex_html");
212        let world = "world";
213        let num = 123;
214        let string = "123".to_string();
215        let string2 = "string2 value".to_string();
216        let user = "123";
217        let active = true;
218        let disabled = false;
219        let selected = "1";
220
221        #[renderable(flow-menu-item)]
222        struct FlowMenuItem {
223            pub text: String,
224            pub value: String,
225            pub children: Option<std::sync::Arc<dyn Render>>,
226        }
227
228        let name2 = "aaa".to_string();
229        let name3 = "bbb".to_string();
230        let tree = tree! {
231            <div class={"abc"} ?active ?disabled ?active2={false} user data-user-name="test-node" string2>
232                123 "hello" {world} {num} {num} {num} {string} {true}
233                {1.2_f64}
234                <h1>"hello 123" {num}</h1>
235                "10"
236                11
237                12 13 14
238                <h3>"single child"</h3>
239                <flow-select ?active name=name2 selected="<1&2>\"3" />
240                <div class="abc"></div>
241                <flow-select ?active name=name3 selected>
242                    <flow text="abc" />
243                    <FlowMenuItem text="abc" value="abc" />
244                </flow-select>
245            </div>
246        };
247
248        let result = tree.html();
249        println!("tag: {:#?}", tree.tag);
250        println!("html: {}", result);
251        assert_eq!(
252            result,
253            "<div active class=\"abc\" data-user-name=\"test-node\" string2=\"string2 value\" user=\"123\">123helloworld123123123123true1.2<h1>hello 123123</h1>1011121314<h3>single child</h3><flow-select active name=\"aaa\" selected=\"&lt;1&amp;2&gt;&quot;3\"></flow-select><div class=\"abc\"></div><flow-select active name=\"bbb\" selected=\"1\"><flow text=\"abc\"></flow><flow-menu-item text=\"abc\" value=\"abc\"></flow-menu-item></flow-select></div>"
254        );
255    }
256
257    fn print_hr(_title: &str) {
258        //println!("\n☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁\n");
259        println!("\n☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰\n")
260    }
261}