Skip to main content

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
26/// HTML and attribute string escaping helpers.
27pub mod escape;
28/// The [`Html`](interface::Html) container holding rendered roots, hooks and retained renderables.
29pub mod interface;
30/// The [`Render`](render::Render) trait and its implementations for rendering values to DOM/HTML.
31pub mod render;
32/// Browser DOM helpers for accessing the `window`, `document` and elements.
33pub mod utils;
34pub use interface::{Hooks, Html};
35
36pub use escape::{escape_attr, escape_html};
37pub use render::{Render, Renderables, Result, Write};
38use std::collections::BTreeMap;
39use std::sync::{Arc, Mutex};
40pub use utils::{Element as WebElement, ElementResult, document};
41use wasm_bindgen::JsCast;
42use wasm_bindgen::prelude::*;
43pub use workflow_html_macros::{html, html_str, renderable, tree};
44
45/// The value of an HTML element attribute.
46#[derive(Debug, Clone)]
47pub enum AttributeValue {
48    /// A boolean attribute that is rendered only when `true`.
49    Bool(bool),
50    /// A string-valued attribute.
51    Str(String),
52}
53
54/// A retained WASM closure handling a DOM `click` mouse event.
55pub type OnClickClosure = Closure<dyn FnMut(web_sys::MouseEvent)>;
56
57/// A single HTML element node carrying its tag, attributes, optional children
58/// and an optional click handler, used to build and render the DOM tree.
59#[derive(Debug, Default, Clone)]
60pub struct Element<T: Render> {
61    /// When `true`, the element renders only its children without wrapping tags.
62    pub is_fragment: bool,
63    /// The element's tag name (e.g. `div`, `flow-select`).
64    pub tag: String,
65    /// The element's attributes keyed by name.
66    pub attributes: BTreeMap<String, AttributeValue>,
67    /// The element's child content, if any.
68    pub children: Option<T>,
69    /// Optional `@name` binding as a `(name, value)` pair recorded in the hooks map.
70    pub reff: Option<(String, String)>,
71    /// Retained click-event closure shared across clones of this element.
72    pub onclick: Arc<Mutex<Option<OnClickClosure>>>,
73}
74
75impl<T: Render + Clone + 'static> Element<T> {
76    /// Registers an event callback on this element. Currently only the
77    /// `"click"` event is wired up, attaching `cb` as a retained closure that
78    /// receives the mouse event and the target [`WebElement`].
79    pub fn on(self, name: &str, cb: Box<dyn Fn(web_sys::MouseEvent, WebElement)>) -> Self {
80        if name.eq("click") {
81            let mut onclick = self.onclick.lock().unwrap();
82            *onclick = Some(Closure::<dyn FnMut(web_sys::MouseEvent)>::new(Box::new(
83                move |event: web_sys::MouseEvent| {
84                    let target = event.target().unwrap().dyn_into::<WebElement>().unwrap();
85                    cb(event, target)
86                },
87            )));
88        }
89        self
90    }
91    //self_.home_item.element.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())?;
92}
93
94/// Provides default access to the rendered attribute and child markup of an
95/// element, used by generated renderable implementations.
96pub trait ElementDefaults {
97    /// Returns the serialized attribute string for this element.
98    fn _get_attributes(&self) -> String;
99    /// Returns the serialized inner HTML of this element's children.
100    fn _get_children(&self) -> String;
101
102    /// Returns the element's attributes as an HTML attribute string.
103    fn get_attributes(&self) -> String {
104        self._get_attributes()
105    }
106    /// Returns the element's children rendered as an HTML string.
107    fn get_children(&self) -> String {
108        self._get_children()
109    }
110}
111
112impl<T: Render + Clone + 'static> Render for Element<T> {
113    fn render_node(
114        self,
115        parent: &mut WebElement,
116        map: &mut Hooks,
117        renderables: &mut Renderables,
118    ) -> ElementResult<()> {
119        renderables.push(Arc::new(self.clone()));
120        let mut el = document().create_element(&self.tag)?;
121
122        let onclick = self.onclick.lock().unwrap();
123        if let Some(onclick) = onclick.as_ref() {
124            el.add_event_listener_with_callback("click", onclick.as_ref().unchecked_ref())?;
125        }
126
127        for (key, value) in &self.attributes {
128            match value {
129                AttributeValue::Bool(v) => {
130                    if *v {
131                        el.set_attribute(key, "true")?;
132                    }
133                }
134                AttributeValue::Str(v) => {
135                    el.set_attribute(key, &escape_attr(v))?;
136                }
137            }
138        }
139        if let Some((key, value)) = self.reff {
140            el.set_attribute("data-ref", &value)?;
141            map.insert(key, el.clone());
142        }
143        if let Some(children) = self.children {
144            children.render_node(&mut el, map, renderables)?;
145        }
146
147        parent.append_child(&el)?;
148        Ok(())
149    }
150    fn render(&self, w: &mut Vec<String>) -> ElementResult<()> {
151        if self.is_fragment {
152            if let Some(children) = &self.children {
153                children.render(w)?;
154            }
155        } else {
156            w.push(format!("<{}", self.tag));
157            for (key, value) in &self.attributes {
158                match value {
159                    AttributeValue::Bool(v) => {
160                        if *v {
161                            w.push(format!(" {key}"));
162                        }
163                    }
164                    AttributeValue::Str(v) => {
165                        w.push(format!(" {}=\"{}\"", key, escape_attr(v)));
166                    }
167                }
168            }
169            w.push(">".to_string());
170
171            if let Some(children) = &self.children {
172                children.render(w)?;
173            }
174            w.push(format!("</{}>", self.tag));
175        }
176        Ok(())
177    }
178
179    fn remove_event_listeners(&self) -> ElementResult<()> {
180        *self.onclick.lock().unwrap() = None;
181        if let Some(children) = &self.children {
182            children.remove_event_listeners()?;
183        }
184        Ok(())
185    }
186}
187
188#[cfg(test)]
189mod test {
190    //cargo test -- --nocapture --test-threads=1
191    use crate as workflow_html;
192    use crate::*;
193    #[test]
194    pub fn simple_html() {
195        self::print_hr("simple_html");
196        let active = "true";
197        let tree = tree! {
198            <p>
199                <div class="xyz abc active" active>{"some inner html"}</div>
200                <div class={"abc"}>"xyz"</div>
201            </p>
202        };
203        let result = tree.html();
204        println!("html: {}", result);
205        assert_eq!(
206            result,
207            "<p><div active=\"true\" class=\"xyz abc active\">some inner html</div><div class=\"abc\">xyz</div></p>"
208        );
209    }
210    #[test]
211    pub fn custom_elements() {
212        self::print_hr("simple_html");
213        let tree = tree! {
214            <flow-select>
215                <flow-menu-item class="xyz" />
216                <flow-menu-item class={"abc"} />
217            </flow-select>
218        };
219        let result = tree.html();
220        println!("html: {}", result);
221        assert_eq!(
222            result,
223            "<flow-select><flow-menu-item class=\"xyz\"></flow-menu-item><flow-menu-item class=\"abc\"></flow-menu-item></flow-select>"
224        );
225    }
226    #[test]
227    pub fn without_root_element() {
228        self::print_hr("without_root_element");
229        let tree = tree! {
230            <div class="xyz"></div>
231            <div class={"abc"}></div>
232        };
233        let result = tree.html();
234        println!("html: {}", result);
235        assert_eq!(result, "<div class=\"xyz\"></div><div class=\"abc\"></div>");
236    }
237    #[test]
238    pub fn complex_html() {
239        self::print_hr("complex_html");
240        let world = "world";
241        let num = 123;
242        let string = "123".to_string();
243        let string2 = "string2 value".to_string();
244        let user = "123";
245        let active = true;
246        let disabled = false;
247        let selected = "1";
248
249        #[renderable(flow-menu-item)]
250        struct FlowMenuItem {
251            pub text: String,
252            pub value: String,
253            pub children: Option<std::sync::Arc<dyn Render>>,
254        }
255
256        let name2 = "aaa".to_string();
257        let name3 = "bbb".to_string();
258        let tree = tree! {
259            <div class={"abc"} ?active ?disabled ?active2={false} user data-user-name="test-node" string2>
260                123 "hello" {world} {num} {num} {num} {string} {true}
261                {1.2_f64}
262                <h1>"hello 123" {num}</h1>
263                "10"
264                11
265                12 13 14
266                <h3>"single child"</h3>
267                <flow-select ?active name=name2 selected="<1&2>\"3" />
268                <div class="abc"></div>
269                <flow-select ?active name=name3 selected>
270                    <flow text="abc" />
271                    <FlowMenuItem text="abc" value="abc" />
272                </flow-select>
273            </div>
274        };
275
276        let result = tree.html();
277        println!("tag: {:#?}", tree.tag);
278        println!("html: {}", result);
279        assert_eq!(
280            result,
281            "<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>"
282        );
283    }
284
285    fn print_hr(_title: &str) {
286        //println!("\n☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁☁\n");
287        println!("\n☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰\n")
288    }
289}