1use crate::attributes::Attribute;
2use crate::events::{Event, EventHandler};
3use crate::node::{IntoNode, Node};
4use crate::reactive::{IntoReactiveBool, IntoReactiveString};
5
6pub struct Element {
7 tag: &'static str,
8 attributes: Vec<Attribute>,
9 children: Vec<Node>,
10 event_handlers: Vec<EventHandler>,
11}
12
13impl Element {
14 pub fn new(tag: &'static str) -> Self {
15 Self {
16 tag,
17 attributes: Vec::new(),
18 children: Vec::new(),
19 event_handlers: Vec::new(),
20 }
21 }
22
23 pub fn tag(&self) -> &'static str {
24 self.tag
25 }
26
27 pub fn attributes(&self) -> &[Attribute] {
28 &self.attributes
29 }
30
31 pub fn get_children(&self) -> &[Node] {
32 &self.children
33 }
34
35 pub fn class(mut self, class: &str) -> Self {
36 self.attributes.push(Attribute::new("class", class));
37 self
38 }
39
40 pub fn class_reactive(mut self, class: impl IntoReactiveString) -> Self {
41 self.attributes.push(Attribute::reactive_string(
42 "class",
43 class.into_reactive_string(),
44 ));
45 self
46 }
47
48 pub fn visible_reactive(mut self, visible: impl IntoReactiveBool) -> Self {
49 self.attributes.push(Attribute::reactive_bool(
50 "data-visible",
51 visible.into_reactive_bool(),
52 ));
53 self
54 }
55
56 pub fn id(mut self, id: &str) -> Self {
57 self.attributes.push(Attribute::new("id", id));
58 self
59 }
60
61 pub fn style(mut self, style: &str) -> Self {
62 self.attributes.push(Attribute::new("style", style));
63 self
64 }
65
66 pub fn styled(self, style: crate::style::Style) -> Self {
67 let css = style.to_css();
68 if css.is_empty() {
69 self
70 } else {
71 self.style(&css)
72 }
73 }
74
75 pub fn attr(mut self, name: &str, value: &str) -> Self {
76 self.attributes.push(Attribute::new(name, value));
77 self
78 }
79
80 pub fn href(mut self, href: &str) -> Self {
81 self.attributes.push(Attribute::new("href", href));
82 self
83 }
84
85 pub fn src(mut self, src: &str) -> Self {
86 self.attributes.push(Attribute::new("src", src));
87 self
88 }
89
90 pub fn alt(mut self, alt: &str) -> Self {
91 self.attributes.push(Attribute::new("alt", alt));
92 self
93 }
94
95 pub fn type_(mut self, type_value: &str) -> Self {
96 self.attributes.push(Attribute::new("type", type_value));
97 self
98 }
99
100 pub fn input_type(self, input_type: crate::types::InputType) -> Self {
101 self.type_(input_type.as_str())
102 }
103
104 pub fn target(mut self, target: crate::types::LinkTarget) -> Self {
105 self.attributes
106 .push(Attribute::new("target", target.as_str()));
107 self
108 }
109
110 pub fn method(mut self, method: crate::types::FormMethod) -> Self {
111 self.attributes
112 .push(Attribute::new("method", method.as_str()));
113 self
114 }
115
116 pub fn name(mut self, name: &str) -> Self {
117 self.attributes.push(Attribute::new("name", name));
118 self
119 }
120
121 pub fn value(mut self, value: &str) -> Self {
122 self.attributes.push(Attribute::new("value", value));
123 self
124 }
125
126 pub fn value_reactive(mut self, value: impl IntoReactiveString) -> Self {
127 self.attributes.push(Attribute::reactive_string(
128 "value",
129 value.into_reactive_string(),
130 ));
131 self
132 }
133
134 pub fn bind_value(
135 self,
136 read: react_rs_core::signal::ReadSignal<String>,
137 write: react_rs_core::signal::WriteSignal<String>,
138 ) -> Self {
139 use crate::reactive::SignalExt;
140 self.value_reactive(read.map(|s| s.clone()))
141 .on_input(move |e| {
142 write.set(e.value().to_string());
143 })
144 }
145
146 pub fn placeholder(mut self, placeholder: &str) -> Self {
147 self.attributes
148 .push(Attribute::new("placeholder", placeholder));
149 self
150 }
151
152 pub fn disabled(mut self, disabled: bool) -> Self {
153 self.attributes
154 .push(Attribute::boolean("disabled", disabled));
155 self
156 }
157
158 pub fn text(mut self, text: impl Into<String>) -> Self {
159 self.children.push(Node::Text(text.into()));
160 self
161 }
162
163 pub fn text_reactive(mut self, text: impl IntoReactiveString) -> Self {
164 self.children
165 .push(Node::ReactiveText(text.into_reactive_string()));
166 self
167 }
168
169 pub fn child(mut self, child: impl IntoNode) -> Self {
170 self.children.push(child.into_node());
171 self
172 }
173
174 pub fn children<I, C>(mut self, children: I) -> Self
175 where
176 I: IntoIterator<Item = C>,
177 C: IntoNode,
178 {
179 for child in children {
180 self.children.push(child.into_node());
181 }
182 self
183 }
184
185 pub fn on_click<F>(mut self, handler: F) -> Self
186 where
187 F: Fn(Event) + 'static,
188 {
189 self.event_handlers
190 .push(EventHandler::new("click", handler));
191 self
192 }
193
194 pub fn on_input<F>(mut self, handler: F) -> Self
195 where
196 F: Fn(Event) + 'static,
197 {
198 self.event_handlers
199 .push(EventHandler::new("input", handler));
200 self
201 }
202
203 pub fn on_submit<F>(mut self, handler: F) -> Self
204 where
205 F: Fn(Event) + 'static,
206 {
207 self.event_handlers
208 .push(EventHandler::new("submit", handler));
209 self
210 }
211
212 pub fn on_change<F>(mut self, handler: F) -> Self
213 where
214 F: Fn(Event) + 'static,
215 {
216 self.event_handlers
217 .push(EventHandler::new("change", handler));
218 self
219 }
220
221 pub fn show_when(self, condition: impl IntoReactiveBool) -> crate::node::Node {
222 crate::node::Node::Conditional(
223 condition.into_reactive_bool(),
224 Box::new(self.into_node()),
225 None,
226 )
227 }
228
229 pub fn show_when_else(
230 self,
231 condition: impl IntoReactiveBool,
232 else_node: impl IntoNode,
233 ) -> crate::node::Node {
234 crate::node::Node::Conditional(
235 condition.into_reactive_bool(),
236 Box::new(self.into_node()),
237 Some(Box::new(else_node.into_node())),
238 )
239 }
240
241 pub fn has_class(&self, class_name: &str) -> bool {
242 self.attributes.iter().any(|attr| {
243 attr.name == "class" && matches!(&attr.value, crate::attributes::AttributeValue::String(v) if v.contains(class_name))
244 })
245 }
246
247 pub fn event_handlers(&self) -> &[EventHandler] {
248 &self.event_handlers
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use crate::html::*;
255 use crate::reactive::SignalExt;
256 use react_rs_core::signal::create_signal;
257
258 #[test]
259 fn test_element_api() {
260 let element = div().class("container").child(h1().text("Hello"));
261
262 assert_eq!(element.tag(), "div");
263 assert!(element.has_class("container"));
264 }
265
266 #[test]
267 fn test_nested_elements() {
268 let view = div()
269 .class("app")
270 .child(nav().class("sidebar").child(ul().children([
271 li().child(a().href("/").text("Home")),
272 li().child(a().href("/about").text("About")),
273 ])))
274 .child(main_el().class("content").child(h1().text("Welcome")));
275
276 assert_eq!(view.tag(), "div");
277 assert!(view.has_class("app"));
278 assert_eq!(view.get_children().len(), 2);
279 }
280
281 #[test]
282 fn test_event_handlers() {
283 let clicked = std::cell::Cell::new(false);
284 let _button = button().text("Click me").on_click(|_| {});
285
286 assert!(!clicked.get());
287 }
288
289 #[test]
290 fn test_reactive_text() {
291 let (count, _set_count) = create_signal(0);
292 let _element = div().text_reactive(count.map(|n| format!("Count: {}", n)));
293 }
294
295 #[test]
296 fn test_reactive_class() {
297 let (active, _set_active) = create_signal(false);
298 let _element = div()
299 .class_reactive(active.map(|a| if *a { "active" } else { "inactive" }.to_string()));
300 }
301}