1pub 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 }
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 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=\"<1&2>"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")
260 }
261}