pub mod escape;
pub mod interface;
pub mod render;
pub mod utils;
pub use interface::{Hooks, Html};
pub use escape::{escape_attr, escape_html};
pub use render::{Render, Renderables, Result, Write};
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
pub use utils::{document, Element as WebElement, ElementResult};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
pub use workflow_html_macros::{html, html_str, renderable, tree};
#[derive(Debug, Clone)]
pub enum AttributeValue {
Bool(bool),
Str(String),
}
pub type OnClickClosure = Closure<dyn FnMut(web_sys::MouseEvent)>;
#[derive(Debug, Default, Clone)]
pub struct Element<T: Render> {
pub is_fragment: bool,
pub tag: String,
pub attributes: BTreeMap<String, AttributeValue>,
pub children: Option<T>,
pub reff: Option<(String, String)>,
pub onclick: Arc<Mutex<Option<OnClickClosure>>>,
}
impl<T: Render + Clone + 'static> Element<T> {
pub fn on(self, name: &str, cb: Box<dyn Fn(web_sys::MouseEvent, WebElement)>) -> Self {
if name.eq("click") {
let mut onclick = self.onclick.lock().unwrap();
*onclick = Some(Closure::<dyn FnMut(web_sys::MouseEvent)>::new(Box::new(
move |event: web_sys::MouseEvent| {
let target = event.target().unwrap().dyn_into::<WebElement>().unwrap();
cb(event, target)
},
)));
}
self
}
}
pub trait ElementDefaults {
fn _get_attributes(&self) -> String;
fn _get_children(&self) -> String;
fn get_attributes(&self) -> String {
self._get_attributes()
}
fn get_children(&self) -> String {
self._get_children()
}
}
impl<T: Render + Clone + 'static> Render for Element<T> {
fn render_node(
self,
parent: &mut WebElement,
map: &mut Hooks,
renderables: &mut Renderables,
) -> ElementResult<()> {
renderables.push(Arc::new(self.clone()));
let mut el = document().create_element(&self.tag)?;
let onclick = self.onclick.lock().unwrap();
if let Some(onclick) = onclick.as_ref() {
el.add_event_listener_with_callback("click", onclick.as_ref().unchecked_ref())?;
}
for (key, value) in &self.attributes {
match value {
AttributeValue::Bool(v) => {
if *v {
el.set_attribute(key, "true")?;
}
}
AttributeValue::Str(v) => {
el.set_attribute(key, &escape_attr(v))?;
}
}
}
if let Some((key, value)) = self.reff {
el.set_attribute("data-ref", &value)?;
map.insert(key, el.clone());
}
if let Some(children) = self.children {
children.render_node(&mut el, map, renderables)?;
}
parent.append_child(&el)?;
Ok(())
}
fn render(&self, w: &mut Vec<String>) -> ElementResult<()> {
if self.is_fragment {
if let Some(children) = &self.children {
children.render(w)?;
}
} else {
w.push(format!("<{}", self.tag));
for (key, value) in &self.attributes {
match value {
AttributeValue::Bool(v) => {
if *v {
w.push(format!(" {key}"));
}
}
AttributeValue::Str(v) => {
w.push(format!(" {}=\"{}\"", key, escape_attr(v)));
}
}
}
w.push(">".to_string());
if let Some(children) = &self.children {
children.render(w)?;
}
w.push(format!("</{}>", self.tag));
}
Ok(())
}
fn remove_event_listeners(&self) -> ElementResult<()> {
*self.onclick.lock().unwrap() = None;
if let Some(children) = &self.children {
children.remove_event_listeners()?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use crate as workflow_html;
use crate::*;
#[test]
pub fn simple_html() {
self::print_hr("simple_html");
let active = "true";
let tree = tree! {
<p>
<div class="xyz abc active" active>{"some inner html"}</div>
<div class={"abc"}>"xyz"</div>
</p>
};
let result = tree.html();
println!("html: {}", result);
assert_eq!(
result,
"<p><div active=\"true\" class=\"xyz abc active\">some inner html</div><div class=\"abc\">xyz</div></p>"
);
}
#[test]
pub fn custom_elements() {
self::print_hr("simple_html");
let tree = tree! {
<flow-select>
<flow-menu-item class="xyz" />
<flow-menu-item class={"abc"} />
</flow-select>
};
let result = tree.html();
println!("html: {}", result);
assert_eq!(result, "<flow-select><flow-menu-item class=\"xyz\"></flow-menu-item><flow-menu-item class=\"abc\"></flow-menu-item></flow-select>");
}
#[test]
pub fn without_root_element() {
self::print_hr("without_root_element");
let tree = tree! {
<div class="xyz"></div>
<div class={"abc"}></div>
};
let result = tree.html();
println!("html: {}", result);
assert_eq!(result, "<div class=\"xyz\"></div><div class=\"abc\"></div>");
}
#[test]
pub fn complex_html() {
self::print_hr("complex_html");
let world = "world";
let num = 123;
let string = "123".to_string();
let string2 = "string2 value".to_string();
let user = "123";
let active = true;
let disabled = false;
let selected = "1";
#[renderable(flow-menu-item)]
struct FlowMenuItem {
pub text: String,
pub value: String,
pub children: Option<std::sync::Arc<dyn Render>>,
}
let name2 = "aaa".to_string();
let name3 = "bbb".to_string();
let tree = tree! {
<div class={"abc"} ?active ?disabled ?active2={false} user data-user-name="test-node" string2>
123 "hello" {world} {num} {num} {num} {string} {true}
{1.2 as f64}
<h1>"hello 123" {num}</h1>
"10"
11
12 13 14
<h3>"single child"</h3>
<flow-select ?active name=name2 selected="<1&2>\"3" />
<div class="abc"></div>
<flow-select ?active name=name3 selected>
<flow text="abc" />
<FlowMenuItem text="abc" value="abc" />
</flow-select>
</div>
};
let result = tree.html();
println!("tag: {:#?}", tree.tag);
println!("html: {}", result);
assert_eq!(
result,
"<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>"
);
}
fn print_hr<'a>(_title: &'a str) {
println!("\n☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰☰\n")
}
}