use super::WebContext;
use concoct::{
body::Child,
hook::{use_context, use_on_drop, use_provider, use_ref},
Body, View,
};
use std::{borrow::Cow, cell::RefCell, rc::Rc};
use web_sys::{
wasm_bindgen::{closure::Closure, JsCast},
Element, Event,
};
macro_rules! make_tag_fns {
($($name:tt),*) => {
$(
pub fn $name<C: Body>(child: C) -> Html<C> {
Html::new(stringify!($name), child)
}
)*
};
}
make_tag_fns!(
a, abbr, address, area, article, aside, audio, b, base, bdi, bdo, blockquote, body, br, button,
canvas, caption, cite, code, col, colgroup, data, datalist, dd, del, details, dfn, dialog, div,
dl, dt, em, embed, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, head,
header, hr, html, i, iframe, img, input, ins, kbd, label, legend, li, link, main, map, mark,
meta, meter, nav, noscript, object, ol, optgroup, option, output, p, param, picture, pre,
progress, q, rp, rt, ruby, s, samp, script, section, select, small, source, span, strong, sub,
summary, sup, svg, table, tbody, td, template, textarea, tfoot, th, thead, time, title, tr,
track, u, ul, var, video, wbr
);
#[derive(Default)]
struct Data {
element: Option<Element>,
callbacks: Vec<(
Closure<dyn FnMut(Event)>,
Rc<RefCell<Rc<RefCell<dyn FnMut(Event)>>>>,
)>,
}
pub struct Html<C> {
tag: Cow<'static, str>,
attrs: Vec<(Cow<'static, str>, Cow<'static, str>)>,
handlers: Vec<(Cow<'static, str>, Rc<RefCell<dyn FnMut(Event)>>)>,
child: Child<C>,
}
macro_rules! impl_handler_methods {
($($fn_name: tt: $name: tt),*) => {
$(
pub fn $fn_name(self, handler: impl FnMut(Event) + 'static) -> Self {
self.handler($name, handler)
}
)*
};
}
impl<C> Html<C> {
pub fn new(tag: impl Into<Cow<'static, str>>, child: C) -> Self {
Self {
tag: tag.into(),
attrs: Vec::new(),
handlers: Vec::new(),
child: Child::new(child),
}
}
pub fn attr(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>,
) -> Self {
self.attrs.push((name.into(), value.into()));
self
}
pub fn handler(
mut self,
name: impl Into<Cow<'static, str>>,
handler: impl FnMut(Event) + 'static,
) -> Self {
self.handlers
.push((name.into(), Rc::new(RefCell::new(handler))));
self
}
impl_handler_methods!(on_click: "click", on_input: "input", on_submit: "submit");
}
impl<C: Body> View for Html<C> {
fn body(&self) -> impl Body {
let data = use_ref(|| RefCell::new(Data::default()));
let mut data_ref = data.borrow_mut();
let web_cx = use_context::<WebContext>().unwrap();
let data_clone = data.clone();
use_on_drop(move || {
if let Some(element) = &data_clone.borrow_mut().element {
element.remove();
}
});
if data_ref.element.is_none() {
let elem = web_cx.document.create_element(&self.tag).unwrap();
web_cx.parent.append_child(&elem).unwrap();
for (name, value) in &self.attrs {
elem.set_attribute(name, value).unwrap();
}
for (name, handler) in &self.handlers {
let handler_cell = Rc::new(RefCell::new(handler.clone()));
let handler_cell_clone = handler_cell.clone();
let callback: Closure<dyn FnMut(Event)> = Closure::wrap(Box::new(move |event| {
handler_cell.borrow().borrow_mut()(event)
}));
elem.add_event_listener_with_callback(&name, callback.as_ref().unchecked_ref())
.unwrap();
data_ref.callbacks.push((callback, handler_cell_clone));
}
data_ref.element = Some(elem);
} else {
for ((_name, handler), (_callback, cell)) in
self.handlers.iter().zip(&data_ref.callbacks)
{
*cell.borrow_mut() = handler.clone();
}
}
use_provider(WebContext {
window: web_cx.window.clone(),
document: web_cx.document.clone(),
parent: data_ref.element.as_ref().unwrap().clone().into(),
});
self.child.clone()
}
}