1pub mod escape;
28pub mod interface;
30pub mod render;
32pub 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#[derive(Debug, Clone)]
47pub enum AttributeValue {
48 Bool(bool),
50 Str(String),
52}
53
54pub type OnClickClosure = Closure<dyn FnMut(web_sys::MouseEvent)>;
56
57#[derive(Debug, Default, Clone)]
60pub struct Element<T: Render> {
61 pub is_fragment: bool,
63 pub tag: String,
65 pub attributes: BTreeMap<String, AttributeValue>,
67 pub children: Option<T>,
69 pub reff: Option<(String, String)>,
71 pub onclick: Arc<Mutex<Option<OnClickClosure>>>,
73}
74
75impl<T: Render + Clone + 'static> Element<T> {
76 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 }
93
94pub trait ElementDefaults {
97 fn _get_attributes(&self) -> String;
99 fn _get_children(&self) -> String;
101
102 fn get_attributes(&self) -> String {
104 self._get_attributes()
105 }
106 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 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=\"<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>"
282 );
283 }
284
285 fn print_hr(_title: &str) {
286 println!("\n☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰\n")
288 }
289}