1#![allow(
3 clippy::missing_panics_doc,
4 clippy::missing_errors_doc,
5 clippy::must_use_candidate,
6 clippy::module_name_repetitions
7)]
8pub mod element_list;
9mod render;
10use std::{cell::RefCell, collections::HashMap, mem, rc::Rc};
11
12use render::queue_update;
13pub use render::{after_render, render_updates};
14use silkenweb_reactive::{clone, signal::ReadSignal};
15use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
16use web_sys as dom;
17
18pub fn mount(id: &str, elem: impl Into<Element>) {
29 unmount(id);
30 let elem = elem.into();
31
32 document()
33 .get_element_by_id(id)
34 .unwrap_or_else(|| panic!("DOM node id = '{}' must exist", id))
35 .append_child(&elem.dom_element())
36 .unwrap();
37 APPS.with(|apps| apps.borrow_mut().insert(id.to_owned(), elem));
38}
39
40pub fn unmount(id: &str) {
44 if let Some(elem) = APPS.with(|apps| apps.borrow_mut().remove(id)) {
45 elem.dom_element().remove();
46 }
47}
48
49pub fn tag(name: impl AsRef<str>) -> ElementBuilder {
53 ElementBuilder::new(name)
54}
55
56pub struct ElementBuilder {
58 element: ElementData,
59 text_nodes: Vec<dom::Text>,
60}
61
62impl ElementBuilder {
63 pub fn new(tag: impl AsRef<str>) -> Self {
64 ElementBuilder {
65 element: ElementData {
66 dom_element: document().create_element(tag.as_ref()).unwrap(),
67 children: Vec::new(),
68 event_callbacks: Vec::new(),
69 reactive_attrs: HashMap::new(),
70 reactive_text: Vec::new(),
71 reactive_with_dom: Vec::new(),
72 },
73 text_nodes: Vec::new(),
74 }
75 }
76
77 pub fn attribute<T>(mut self, name: impl AsRef<str>, value: impl AttributeValue<T>) -> Self {
79 value.set_attribute(name, &mut self);
80 mem::drop(value);
81 self
82 }
83
84 pub fn child(mut self, child: impl Into<Element>) -> Self {
87 let child = child.into();
88
89 self.append_child(&child.dom_element());
90 self.element.children.push(child);
91 self
92 }
93
94 pub fn text(mut self, child: impl Text) -> Self {
96 child.set_text(&mut self);
97 mem::drop(child);
98 self
99 }
100
101 pub fn effect<T>(mut self, child: impl Effect<T>) -> Self {
125 child.set_effect(&mut self);
126 self
127 }
128
129 pub fn on(mut self, name: &'static str, f: impl 'static + FnMut(JsValue)) -> Self {
138 {
139 let dom_element = self.element.dom_element.clone();
140 self.element
141 .event_callbacks
142 .push(EventCallback::new(dom_element, name, f));
143 }
144
145 self
146 }
147
148 fn insert_child_before(&mut self, new_node: &dom::Node, reference_node: &dom::Node) {
149 let dom_element = self.element.dom_element.clone();
150 clone!(new_node, reference_node);
151
152 queue_update(move || {
153 dom_element
154 .insert_before(&new_node, Some(&reference_node))
155 .unwrap();
156 });
157 }
158
159 fn append_child(&mut self, element: &dom::Node) {
160 let dom_element = self.element.dom_element.clone();
161 clone!(element);
162
163 queue_update(move || {
164 dom_element.append_child(&element).unwrap();
165 });
166 }
167
168 fn remove_child(&mut self, element: &dom::Node) {
169 let dom_element = self.element.dom_element.clone();
170 clone!(element);
171
172 queue_update(move || {
173 dom_element.remove_child(&element).unwrap();
174 });
175 }
176}
177
178impl Builder for ElementBuilder {
179 type Target = Element;
180
181 fn build(self) -> Self::Target {
182 Element(Rc::new(ElementKind::Static(self.element)))
183 }
184
185 fn into_element(self) -> Element {
186 self.build()
187 }
188}
189
190impl DomElement for ElementBuilder {
191 type Target = dom::Element;
192
193 fn dom_element(&self) -> Self::Target {
194 self.element.dom_element.clone()
195 }
196}
197
198impl From<ElementBuilder> for Element {
199 fn from(builder: ElementBuilder) -> Self {
200 builder.build()
201 }
202}
203
204#[derive(Clone)]
209pub struct Element(Rc<ElementKind>);
210
211impl DomElement for Element {
212 type Target = dom::Element;
213
214 fn dom_element(&self) -> Self::Target {
215 match self.0.as_ref() {
216 ElementKind::Static(elem) => elem.dom_element.clone(),
217 ElementKind::Reactive(elem) => elem.current().clone(),
218 }
219 }
220}
221
222impl<E> From<ReadSignal<E>> for Element
223where
224 E: 'static + DomElement,
225{
226 fn from(elem: ReadSignal<E>) -> Self {
227 let dom_element = Rc::new(RefCell::new(elem.current().dom_element().into()));
228
229 let updater = elem.map({
230 move |element| {
231 let new_dom_element: dom::Element = element.dom_element().into();
232
233 queue_update({
234 let dom_element = dom_element.borrow().clone();
235 clone!(new_dom_element);
236
237 move || {
238 dom_element
239 .replace_with_with_node_1(&new_dom_element)
240 .unwrap();
241 }
242 });
243
244 dom_element.replace(new_dom_element.clone());
245 new_dom_element
246 }
247 });
248
249 Self(Rc::new(ElementKind::Reactive(updater)))
250 }
251}
252
253impl Builder for Element {
254 type Target = Self;
255
256 fn build(self) -> Self::Target {
257 self
258 }
259
260 fn into_element(self) -> Element {
261 self
262 }
263}
264
265pub trait StaticAttribute {
267 fn set_attribute(&self, name: impl AsRef<str>, dom_element: &dom::Element);
268}
269
270impl StaticAttribute for bool {
271 fn set_attribute(&self, name: impl AsRef<str>, dom_element: &dom::Element) {
272 clone!(dom_element);
273 let name = name.as_ref().to_string();
274
275 if *self {
276 queue_update(move || {
277 dom_element.set_attribute(&name, "").unwrap();
278 });
279 } else {
280 queue_update(move || {
281 dom_element.remove_attribute(&name).unwrap();
282 });
283 }
284 }
285}
286
287impl StaticAttribute for String {
288 fn set_attribute(&self, name: impl AsRef<str>, dom_element: &dom::Element) {
289 set_attribute(dom_element, name, self);
290 }
291}
292
293impl StaticAttribute for str {
294 fn set_attribute(&self, name: impl AsRef<str>, dom_element: &dom::Element) {
295 set_attribute(dom_element, name, self);
296 }
297}
298
299fn set_attribute(dom_element: &dom::Element, name: impl AsRef<str>, value: impl AsRef<str>) {
300 clone!(dom_element);
301 let name = name.as_ref().to_string();
302 let value = value.as_ref().to_string();
303
304 queue_update(move || dom_element.set_attribute(&name, &value).unwrap());
305}
306
307pub trait AttributeValue<T> {
309 fn set_attribute(&self, name: impl AsRef<str>, builder: &mut ElementBuilder);
310}
311
312impl<T> AttributeValue<T> for T
313where
314 T: StaticAttribute,
315{
316 fn set_attribute(&self, name: impl AsRef<str>, builder: &mut ElementBuilder) {
317 self.set_attribute(name, &builder.element.dom_element);
318 }
319}
320
321impl<'a> AttributeValue<String> for &'a str {
322 fn set_attribute(&self, name: impl AsRef<str>, builder: &mut ElementBuilder) {
323 set_attribute(&builder.element.dom_element, name, self);
324 }
325}
326
327impl<'a> AttributeValue<String> for &'a String {
328 fn set_attribute(&self, name: impl AsRef<str>, builder: &mut ElementBuilder) {
329 set_attribute(&builder.element.dom_element, name, self);
330 }
331}
332
333impl AttributeValue<String> for ReadSignal<&'static str> {
334 fn set_attribute(&self, name: impl AsRef<str>, builder: &mut ElementBuilder) {
335 self.map(|&value| value.to_string())
336 .set_attribute(name, builder);
337 }
338}
339
340impl<T> AttributeValue<T> for ReadSignal<T>
341where
342 T: 'static + StaticAttribute,
343{
344 fn set_attribute(&self, name: impl AsRef<str>, builder: &mut ElementBuilder) {
345 let name = name.as_ref().to_string();
346 let dom_element = builder.element.dom_element.clone();
347 self.current().set_attribute(&name, &dom_element);
348
349 let updater = self.map({
350 clone!(name);
351 move |new_value| {
352 new_value.set_attribute(&name, &dom_element);
353 }
354 });
355
356 builder.element.reactive_attrs.insert(name, updater);
357 }
358}
359
360impl<'a, T> AttributeValue<T> for &'a ReadSignal<T>
361where
362 ReadSignal<T>: AttributeValue<T>,
363 T: 'static,
364{
365 fn set_attribute(&self, name: impl AsRef<str>, builder: &mut ElementBuilder) {
366 (*self).set_attribute(name, builder);
367 }
368}
369
370pub trait Effect<T> {
372 fn set_effect(self, builder: &mut ElementBuilder);
373}
374
375impl<F, T> Effect<T> for F
376where
377 F: 'static + Fn(&T),
378 T: 'static + JsCast,
379{
380 fn set_effect(self, builder: &mut ElementBuilder) {
381 let dom_element = builder.dom_element().dyn_into().unwrap();
382 after_render(move || self(&dom_element))
383 }
384}
385
386impl<F, T> Effect<T> for ReadSignal<F>
387where
388 F: 'static + Clone + Fn(&T),
389 T: 'static + Clone + JsCast,
390{
391 fn set_effect(self, builder: &mut ElementBuilder) {
392 let dom_element: T = builder.dom_element().dyn_into().unwrap();
393 let current = self.current().clone();
394
395 after_render({
396 clone!(dom_element);
397 move || current(&dom_element)
398 });
399
400 let updater = self.map(move |new_value| {
401 after_render({
402 clone!(new_value, dom_element);
403 move || new_value(&dom_element)
404 })
405 });
406
407 builder.element.reactive_with_dom.push(updater);
408 }
409}
410
411pub trait Text {
413 fn set_text(&self, builder: &mut ElementBuilder);
414}
415
416fn set_static_text<T: AsRef<str>>(text: &T, builder: &mut ElementBuilder) {
417 let text_node = document().create_text_node(text.as_ref());
418 builder.append_child(&text_node);
419 builder.text_nodes.push(text_node);
420}
421
422impl<'a> Text for &'a str {
423 fn set_text(&self, builder: &mut ElementBuilder) {
424 set_static_text(self, builder)
425 }
426}
427
428impl<'a> Text for &'a String {
429 fn set_text(&self, builder: &mut ElementBuilder) {
430 set_static_text(self, builder)
431 }
432}
433
434impl Text for String {
435 fn set_text(&self, builder: &mut ElementBuilder) {
436 set_static_text(self, builder)
437 }
438}
439
440impl<T> Text for ReadSignal<T>
441where
442 T: 'static + AsRef<str>,
443{
444 fn set_text(&self, builder: &mut ElementBuilder) {
445 set_static_text(&self.current().as_ref(), builder);
446
447 if let Some(text_node) = builder.text_nodes.last() {
448 let updater = self.map({
449 clone!(text_node);
450
451 move |new_value| {
452 queue_update({
453 clone!(text_node);
454 let new_value = new_value.as_ref().to_string();
455 move || text_node.set_node_value(Some(new_value.as_ref()))
456 });
457 }
458 });
459
460 builder.element.reactive_text.push(updater);
461 }
462 }
463}
464
465impl<'a, T> Text for &'a ReadSignal<T>
466where
467 T: 'static,
468 ReadSignal<T>: Text,
469{
470 fn set_text(&self, builder: &mut ElementBuilder) {
471 (*self).set_text(builder);
472 }
473}
474
475pub trait DomElement {
478 type Target: Into<dom::Element> + AsRef<dom::Element> + Clone;
479
480 fn dom_element(&self) -> Self::Target;
481}
482
483impl<T> DomElement for Option<T>
484where
485 T: DomElement,
486{
487 type Target = dom::Element;
488
489 fn dom_element(&self) -> Self::Target {
490 match self {
491 Some(elem) => elem.dom_element().into(),
492 None => {
493 let none = document().create_element("div").unwrap();
499 none.unchecked_ref::<dom::HtmlElement>().set_hidden(true);
500 none
501 }
502 }
503 }
504}
505
506pub trait Builder {
508 type Target;
509
510 fn build(self) -> Self::Target;
511
512 fn into_element(self) -> Element;
513}
514
515enum ElementKind {
516 Static(ElementData),
517 Reactive(ReadSignal<dom::Element>),
518}
519
520struct ElementData {
521 dom_element: dom::Element,
522 children: Vec<Element>,
523 event_callbacks: Vec<EventCallback>,
524 reactive_attrs: HashMap<String, ReadSignal<()>>,
525 reactive_text: Vec<ReadSignal<()>>,
526 reactive_with_dom: Vec<ReadSignal<()>>,
527}
528
529struct EventCallback {
530 target: dom::Element,
531 name: &'static str,
532 callback: Closure<dyn FnMut(JsValue)>,
533}
534
535impl EventCallback {
536 fn new(target: dom::Element, name: &'static str, f: impl 'static + FnMut(JsValue)) -> Self {
537 let callback = Closure::wrap(Box::new(f) as Box<dyn FnMut(JsValue)>);
538 target
539 .add_event_listener_with_callback(name, callback.as_ref().unchecked_ref())
540 .unwrap();
541
542 Self {
543 target,
544 name,
545 callback,
546 }
547 }
548}
549
550impl Drop for EventCallback {
551 fn drop(&mut self) {
552 self.target
553 .remove_event_listener_with_callback(
554 self.name,
555 self.callback.as_ref().as_ref().unchecked_ref(),
556 )
557 .unwrap();
558 }
559}
560
561fn window() -> dom::Window {
562 dom::window().expect("Window must be available")
563}
564
565fn document() -> dom::Document {
566 window().document().expect("Window must contain a document")
567}
568
569thread_local!(
570 static APPS: RefCell<HashMap<String, Element>> = RefCell::new(HashMap::new());
571);