1use crate::sealed::Sealed;
2use cope::singleton::{react, Atom};
3use wasm_bindgen::{prelude::*, JsCast};
4use web_sys::{window, Element, Event, EventTarget, HtmlButtonElement};
5
6#[must_use]
7pub struct ElementBuilder<E> {
8 element: E,
9}
10
11impl<E> ElementBuilder<E> {
12 pub fn new(element: E) -> Self {
13 ElementBuilder { element }
14 }
15}
16
17impl<E> ElementBuilder<E> {
18 pub fn build(self) -> E {
19 self.element
20 }
21}
22
23impl<E> AsRef<E> for ElementBuilder<E> {
24 fn as_ref(&self) -> &E {
25 &self.element
26 }
27}
28
29impl<E: AsRef<Element>> ElementBuilder<E> {
30 pub fn child(self, value: impl ElementChild) -> Self {
31 value.append(&self);
32 self
33 }
34
35 pub fn class_name(self, value: &str) -> Self {
36 self.element.as_ref().set_class_name(value);
37 self
38 }
39
40 pub fn id(self, value: &str) -> Self {
41 self.element.as_ref().set_id(value);
42 self
43 }
44
45 pub fn on_click(self, f: impl FnMut() + 'static) -> Self {
46 self.element
47 .as_ref()
48 .add_event_listener_with_callback(
49 "click",
50 Closure::wrap(Box::new(f) as Box<dyn FnMut()>)
51 .into_js_value()
52 .unchecked_ref(),
53 )
54 .unwrap_throw();
55 self
56 }
57}
58
59pub trait ElementChild: Sealed {
60 fn append<P: AsRef<Element>>(self, parent: &ElementBuilder<P>);
61}
62
63impl<T: ElementChild> Sealed for T {}
64
65impl<E: AsRef<Element>> ElementChild for ElementBuilder<E> {
66 fn append<P: AsRef<Element>>(self, parent: &ElementBuilder<P>) {
67 let parent = parent.element.as_ref();
68 let node = self.build();
69 parent.append_with_node_1(node.as_ref()).unwrap_throw();
70 }
71}
72
73impl ElementChild for &str {
74 fn append<P: AsRef<Element>>(self, parent: &ElementBuilder<P>) {
75 parent
76 .element
77 .as_ref()
78 .append_with_str_1(self)
79 .unwrap_throw();
80 }
81}
82
83impl ElementChild for String {
84 fn append<P: AsRef<Element>>(self, parent: &ElementBuilder<P>) {
85 ElementChild::append(self.as_str(), parent);
86 }
87}
88
89impl ElementChild for &Atom<String> {
90 fn append<P: AsRef<Element>>(self, parent: &ElementBuilder<P>) {
91 let document = window().unwrap_throw().document().unwrap_throw();
92 let node = document.create_text_node("");
93
94 let parent = parent.as_ref().as_ref();
95 parent.append_with_node_1(&node).unwrap_throw();
96
97 let value = self.clone();
98 react(move || {
99 node.set_node_value(Some(&value.get()));
100 });
101 }
102}
103
104impl<E: AsRef<EventTarget>> ElementBuilder<E> {
105 pub fn add_event_listener_with_callback(
106 self,
107 type_: &str,
108 listener: impl Fn(Event) + 'static,
109 ) -> Self {
110 self.as_ref()
111 .as_ref()
112 .add_event_listener_with_callback(
113 type_,
114 Closure::wrap(Box::new(listener) as Box<dyn Fn(Event)>)
115 .into_js_value()
116 .unchecked_ref(),
117 )
118 .unwrap_throw();
119 self
120 }
121}
122
123impl<E: AsRef<HtmlButtonElement>> ElementBuilder<E> {
124 pub fn type_(self, value: &str) -> Self {
125 self.element.as_ref().set_type(value);
126 self
127 }
128}
129
130macro_rules! define_builder {
131 ($name:ident => $type:ident) => {
132 pub fn $name() -> ElementBuilder<::web_sys::$type> {
133 let document = window().unwrap_throw().document().unwrap_throw();
134 let element = document.create_element(stringify!($name)).unwrap_throw();
135 ElementBuilder::new(element.unchecked_into())
136 }
137 };
138}
139
140define_builder!(a => Element);
141define_builder!(button => HtmlButtonElement);
142define_builder!(div => Element);
143define_builder!(h1 => Element);
144define_builder!(span => Element);
145define_builder!(table => Element);
146define_builder!(tbody => Element);
147define_builder!(td => Element);
148define_builder!(tr => Element);
149
150#[cfg(test)]
151mod tests {
152 use crate::elements::{div, span};
153 use wasm_bindgen_test::wasm_bindgen_test;
154
155 #[wasm_bindgen_test]
156 fn build() {
157 let div = div().build();
158 assert_eq!(div.node_name(), "DIV");
159 }
160
161 #[wasm_bindgen_test]
162 fn child_element() {
163 let div = div().child(span()).build();
164 assert_eq!(div.outer_html(), "<div><span></span></div>");
165 }
166
167 #[wasm_bindgen_test]
168 fn child_str() {
169 let div = div().child("str").build();
170 assert_eq!(div.outer_html(), "<div>str</div>");
171 }
172
173 #[wasm_bindgen_test]
174 fn child_string() {
175 let div = div().child("string".to_string()).build();
176 assert_eq!(div.outer_html(), "<div>string</div>");
177 }
178}