silkenweb_dom/
lib.rs

1//! A reactive interface to the DOM.
2#![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
18/// Mount an element on the document.
19///
20/// `id` is the html element id of the parent element. The element is added as
21/// the last child of this element.
22///
23/// Mounting an `id` that is already mounted will remove that element.
24///
25/// An [`Element`] can only appear once in the document. Adding an [`Element`]
26/// to the document a second time will move it. It will still require
27/// unmounting from both places to free up any resources.
28pub 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
40/// Unmount an element.
41///
42/// This is mostly useful for testing and checking for memory leaks
43pub 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
49/// An HTML element tag.
50///
51/// For example: `tag("div")`
52pub fn tag(name: impl AsRef<str>) -> ElementBuilder {
53    ElementBuilder::new(name)
54}
55
56/// Build an HTML element.
57pub 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    /// Set an attribute. Attribute values can be reactive.
78    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    /// Add a child element after existing children. The child element can be
85    /// reactive.
86    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    /// Add a text node after existing children. The text node can be reactive.
95    pub fn text(mut self, child: impl Text) -> Self {
96        child.set_text(&mut self);
97        mem::drop(child);
98        self
99    }
100
101    /// Apply an effect after the next render. For example, to set the focus of
102    /// an element:
103    ///
104    /// ```no_run
105    /// # use silkenweb_dom::tag;
106    /// # use web_sys::HtmlInputElement;
107    /// # let element = tag("input");
108    /// element.effect(|elem: &HtmlInputElement| elem.focus().unwrap());
109    /// ```
110    ///
111    /// Effects can be reactive. For example, to set the visibibilty of an item
112    /// based on a `hidden` boolean signal:
113    ///
114    /// ```no_run
115    /// # use silkenweb_dom::tag;
116    /// # use silkenweb_reactive::signal::Signal;
117    /// # use web_sys::HtmlInputElement;
118    /// # let element = tag("input");
119    /// let hidden = Signal::new(false);
120    /// let is_hidden = hidden.read();
121    ///
122    /// element.effect(is_hidden.map(|&hidden| move |elem: &HtmlInputElement| elem.set_hidden(hidden)));
123    /// ```
124    pub fn effect<T>(mut self, child: impl Effect<T>) -> Self {
125        child.set_effect(&mut self);
126        self
127    }
128
129    /// Register an event handler.
130    ///
131    /// `name` is the name of the event. See the [MDN Events] page for a list.
132    ///
133    /// `f` is the callback when the event fires and will be passed the
134    /// javascript `Event` object.
135    ///
136    /// [MDN Events]: https://developer.mozilla.org/en-US/docs/Web/Events
137    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/// An HTML element.
205///
206/// Elements can only appear once in the document. If an element is added again,
207/// it will be moved.
208#[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
265/// A non-reactive attribute.
266pub 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
307/// A potentially reactive attribute.
308pub 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
370/// An [`Effect`] that can be applied to an [`Element`] after rendering.
371pub 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
411/// A Text element.
412pub 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
475// TODO(review): Find a better way to add all child types to dom
476/// Get a raw Javascript, non-reactive DOM element.
477pub 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                // We use a hidden `div` element as a placeholder. We'll call
494                // `replace_with_with_node_1` if a reactive option changes to `Some`.
495                //
496                // Comments won't work as their interface is `Node` rather than `Element`, which
497                // means we can't call `replace`.
498                let none = document().create_element("div").unwrap();
499                none.unchecked_ref::<dom::HtmlElement>().set_hidden(true);
500                none
501            }
502        }
503    }
504}
505
506/// An HTML element builder.
507pub 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);