Skip to main content

fusor/
dom.rs

1//! Bind real DOM nodes to Rust closures. The browser creates the HTML DOM.
2//! A `Scope` owns its effects and event listeners. Drop it to unmount behavior.
3
4pub mod application;
5mod bindings;
6mod branch;
7mod children;
8pub mod coherent;
9mod commit;
10mod component;
11mod content;
12#[doc(hidden)]
13pub mod controls;
14#[doc(hidden)]
15pub use children::Children;
16#[cfg(feature = "islands")]
17pub mod delivery;
18mod hydration;
19mod keyed;
20mod mount;
21mod property;
22mod range;
23mod reconcile;
24mod strings;
25mod target;
26#[doc(hidden)]
27pub mod text_value;
28
29pub use content::Content;
30#[doc(hidden)]
31pub use mount::{NestingGuard, TemplateNodes};
32#[doc(hidden)]
33pub use range::{Anchors, MountPoint};
34pub use target::{ElementTarget, InputTarget};
35
36use crate::{Effect, Owner, OwnerHandle, batch};
37use std::{cell::RefCell, collections::VecDeque, rc::Rc, thread::LocalKey};
38pub use wasm_bindgen::JsValue;
39use wasm_bindgen::{JsCast, closure::Closure};
40use web_sys::{Document, Element, Event, EventTarget, HtmlTemplateElement};
41
42/// Implemented by the HTML build for each `rust:component="RustType"`.
43/// State is shared by the generated closures without requiring `Clone`.
44/// Keep the returned scope alive; dropping it releases every binding.
45pub trait Component: Sized + 'static {
46    const TEMPLATE_HASH: &'static str = "";
47    const TEMPLATE_HTML: &'static str = "";
48    fn mount(self) -> Result<Scope, JsValue>;
49
50    /// Construct state after template validation, with its own weak lifetime.
51    fn mount_with(make: impl FnOnce(OwnerHandle) -> Self) -> Result<Scope, JsValue> {
52        Self::try_mount_with(|owner| Ok(make(owner)))
53    }
54
55    fn try_mount_with(
56        make: impl FnOnce(OwnerHandle) -> Result<Self, JsValue>,
57    ) -> Result<Scope, JsValue> {
58        let scope = Self::prepare_component(None, Box::new(make))?;
59        scope.try_commit()?;
60        Ok(scope)
61    }
62
63    /// Prepare a child while owner-activation registrations wait. Ordinary
64    /// constructor effects keep their immediate timing. Attach, then commit the
65    /// returned scope; dropping it rolls back preparation. Used by outlets.
66    fn prepare(
67        parent: &OwnerHandle,
68        make: impl FnOnce(OwnerHandle) -> Result<Self, JsValue>,
69    ) -> Result<Scope, JsValue> {
70        Self::prepare_component(Some(parent), Box::new(make))
71    }
72
73    /// Generated implementations validate the template before calling `make`.
74    /// The default supports hand-written components that implement `mount`.
75    #[doc(hidden)]
76    fn prepare_component(
77        parent: Option<&OwnerHandle>,
78        make: ComponentFactory<'_, Self>,
79    ) -> Result<Scope, JsValue> {
80        let owner = parent.map(Owner::child).unwrap_or_default();
81        let mut scope = make(owner.handle())?.mount()?;
82        // A hand-written mount can return a previously prepared scope. Preserve
83        // its original hydration adoption decision when replacing that owner.
84        if let Some(owned) = scope.hydration_ownership.value(&scope.owner) {
85            scope.hydration_ownership = HydrationOwnership::Preserved(owned);
86        }
87        scope.owner = owner;
88        scope.prepare_queue(parent);
89        Ok(scope)
90    }
91}
92
93/// Erase the factory at the generated-code boundary so recursive components do
94/// not recursively instantiate a different closure type at every nesting level.
95#[doc(hidden)]
96pub type ComponentFactory<'a, C> = Box<dyn FnOnce(OwnerHandle) -> Result<C, JsValue> + 'a>;
97
98/// A reusable component backed by a cloned HTML template, rather than existing DOM.
99/// Generated for types declared with `<template rust:component="Type">`.
100pub trait TemplateComponent: Component {}
101
102/// Compatibility import for the renderer-independent construction contract.
103pub use crate::FromInputs;
104
105/// Convert a component construction error at the browser mounting boundary.
106///
107/// Implement this for a custom portable error when enabling browser rendering.
108/// There is deliberately no blanket `Display` implementation: JavaScript values
109/// retain their original identity, and applications choose how to represent a
110/// native error to the browser.
111pub trait IntoMountError {
112    fn into_mount_error(self) -> JsValue;
113}
114
115impl IntoMountError for JsValue {
116    fn into_mount_error(self) -> JsValue {
117        self
118    }
119}
120
121impl IntoMountError for std::convert::Infallible {
122    fn into_mount_error(self) -> JsValue {
123        match self {}
124    }
125}
126
127impl IntoMountError for String {
128    fn into_mount_error(self) -> JsValue {
129        JsValue::from_str(&self)
130    }
131}
132
133impl IntoMountError for &str {
134    fn into_mount_error(self) -> JsValue {
135        JsValue::from_str(self)
136    }
137}
138
139pub fn document() -> Result<Document, JsValue> {
140    web_sys::window()
141        .and_then(|window| window.document())
142        .ok_or_else(|| JsValue::from_str("fusor requires a browser document"))
143}
144
145/// Create an element using the browser's DOM API, with no markup macro.
146pub fn element(tag: &str) -> Result<Element, JsValue> {
147    document()?.create_element(tag)
148}
149
150fn missing(selector: &str) -> JsValue {
151    JsValue::from_str(&format!("fusor: no element matches {selector:?}"))
152}
153
154const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml";
155
156fn is_html(element: &Element) -> bool {
157    element.namespace_uri().as_deref() == Some(HTML_NAMESPACE)
158}
159
160/// Replace a thread-local for the duration of `run`, restoring it on unwind.
161fn scoped<T: 'static, R>(
162    key: &'static LocalKey<RefCell<T>>,
163    value: T,
164    run: impl FnOnce() -> R,
165) -> R {
166    struct Restore<T: 'static>(&'static LocalKey<RefCell<T>>, Option<T>);
167    impl<T: 'static> Drop for Restore<T> {
168        fn drop(&mut self) {
169            if let Some(previous) = self.1.take() {
170                self.0.set(previous);
171            }
172        }
173    }
174    let _restore = Restore(key, Some(key.replace(value)));
175    run()
176}
177
178/// Find or insert an entry in a small FIFO-bounded registry of static metadata.
179/// No registry borrow crosses `make`, which may reenter through JavaScript.
180fn cached<T: Clone + 'static>(
181    registry: &'static LocalKey<RefCell<VecDeque<T>>>,
182    limit: usize,
183    matches: impl Fn(&T) -> bool,
184    make: impl FnOnce() -> T,
185) -> T {
186    if let Some(found) =
187        registry.with_borrow(|entries| entries.iter().rev().find(|entry| matches(entry)).cloned())
188    {
189        return found;
190    }
191    let entry = make();
192    registry.with_borrow_mut(|entries| {
193        if entries.len() >= limit {
194            entries.pop_front();
195        }
196        entries.push_back(entry.clone());
197    });
198    entry
199}
200
201/// Prepare a component on its server-rendered root, when there is one.
202fn with_native_root<R>(
203    root: Option<&Element>,
204    make: impl FnOnce() -> Result<R, JsValue>,
205) -> Result<R, JsValue> {
206    match root {
207        #[cfg(feature = "islands")]
208        Some(root) => hydration::with_root(root, make),
209        _ => make(),
210    }
211}
212
213/// Remove an owned root, first disposing islands activated inside it.
214fn remove_tree(root: &Element) {
215    #[cfg(feature = "islands")]
216    delivery::dispose_tree(root);
217    root.remove();
218}
219
220type Handler = Rc<dyn Fn(Event)>;
221
222/// Listener handlers, reached from native listeners through one dispatcher.
223/// Releasing a slot advances its generation, so a stale native listener
224/// cannot reach a later handler that reuses the slot.
225#[derive(Default)]
226struct Handlers {
227    slots: Vec<(u32, Option<Handler>)>,
228    free: Vec<u32>,
229}
230
231impl Handlers {
232    fn insert(&mut self, handler: Handler) -> (u32, u32) {
233        if let Some(slot) = self.free.pop() {
234            let entry = &mut self.slots[slot as usize];
235            entry.1 = Some(handler);
236            return (slot, entry.0);
237        }
238        self.slots.push((0, Some(handler)));
239        ((self.slots.len() - 1) as u32, 0)
240    }
241
242    fn get(&self, slot: u32, generation: u32) -> Option<Handler> {
243        let (current, handler) = self.slots.get(slot as usize)?;
244        (*current == generation).then(|| handler.clone())?
245    }
246
247    /// The caller drops the handler after releasing the registry borrow.
248    fn remove(&mut self, slot: u32, generation: u32) -> Option<Handler> {
249        let entry = self
250            .slots
251            .get_mut(slot as usize)
252            .filter(|(current, _)| *current == generation)?;
253        entry.0 = entry.0.wrapping_add(1);
254        self.free.push(slot);
255        entry.1.take()
256    }
257}
258
259thread_local! {
260    static HANDLERS: RefCell<Handlers> = RefCell::new(Handlers::default());
261    // No registry borrow is held while a handler runs: it may add, remove or
262    // dispatch listeners, including its own.
263    static DISPATCH: Closure<dyn Fn(u32, u32, Event)> = Closure::new(|slot, generation, event| {
264        let handler = HANDLERS.with_borrow(|handlers| handlers.get(slot, generation));
265        if let Some(handler) = handler {
266            handler(event);
267        }
268    });
269}
270
271/// Where a listener is attached: a native target, or an entry of a validated
272/// binding bundle, which keeps its original node.
273enum ListenerTarget {
274    Node(EventTarget),
275    Bundle(Rc<JsValue>, u32),
276}
277
278/// A DOM event listener, removed when dropped.
279pub struct Listener {
280    target: ListenerTarget,
281    event: strings::EventName,
282    slot: u32,
283    generation: u32,
284}
285
286impl Listener {
287    /// Call `handler` for each `event` on `target`, as dispatched. Unlike
288    /// [`Scope::on`], signal writes are not batched and no owner gates it.
289    pub fn new(
290        target: EventTarget,
291        event: &str,
292        handler: impl FnMut(Event) + 'static,
293    ) -> Result<Self, JsValue> {
294        Self::attach(ListenerTarget::Node(target), event, handler)
295    }
296
297    fn attach(
298        target: ListenerTarget,
299        event: &str,
300        handler: impl FnMut(Event) + 'static,
301    ) -> Result<Self, JsValue> {
302        let handler = RefCell::new(handler);
303        let handler: Handler = Rc::new(move |event| (handler.borrow_mut())(event));
304        let (slot, generation) = HANDLERS.with_borrow_mut(|handlers| handlers.insert(handler));
305        let event = strings::EventName::from(event);
306        let listening = DISPATCH.with(|dispatch| match &target {
307            ListenerTarget::Node(node) => {
308                strings::listen(node, &event, dispatch.as_ref(), slot, generation)
309            }
310            ListenerTarget::Bundle(nodes, index) => {
311                strings::listen_bundle(nodes, *index, &event, dispatch.as_ref(), slot, generation)
312            }
313        });
314        match listening {
315            Ok(()) => Ok(Self {
316                target,
317                event,
318                slot,
319                generation,
320            }),
321            Err(error) => {
322                let handler =
323                    HANDLERS.with_borrow_mut(|handlers| handlers.remove(slot, generation));
324                drop(handler);
325                Err(error)
326            }
327        }
328    }
329
330    /// Batch the handler's signal writes; skip events while `active` is false.
331    fn batched(
332        target: ListenerTarget,
333        event: &str,
334        active: impl Fn() -> bool + 'static,
335        mut handler: impl FnMut(Event) + 'static,
336    ) -> Result<Self, JsValue> {
337        Self::attach(target, event, move |event| {
338            if active() {
339                batch(|| handler(event));
340            }
341        })
342    }
343}
344
345impl Drop for Listener {
346    fn drop(&mut self) {
347        let _ = match &self.target {
348            ListenerTarget::Node(node) => strings::remove(node, &self.event, self.slot),
349            ListenerTarget::Bundle(nodes, index) => {
350                strings::unlisten_bundle(nodes, *index, &self.event, self.slot)
351            }
352        };
353        let handler =
354            HANDLERS.with_borrow_mut(|handlers| handlers.remove(self.slot, self.generation));
355        drop(handler);
356    }
357}
358
359/// A DOM island, including the lifetime of all of its reactive bindings.
360/// Dropping the scope stops behavior; existing markup remains in the document.
361#[must_use = "retain the scope to keep its DOM bindings and event listeners active"]
362pub struct Scope {
363    owner: Owner,
364    root: Element,
365    fragment: Option<MountPoint>,
366    effects: Vec<Effect>,
367    listeners: Vec<Listener>,
368    children: Vec<Scope>,
369    component_state: Option<Rc<dyn std::any::Any>>,
370    retained: Vec<Box<dyn std::any::Any>>,
371    mount_queue: Option<Rc<commit::CommitQueue>>,
372    mount_parent: Option<OwnerHandle>,
373    remove_on_drop: bool,
374    render_tree: Option<Rc<coherent::Tree>>,
375    hydrating: bool,
376    hydration_ownership: HydrationOwnership,
377}
378
379// Generated prepared scopes read their final owner's monotone activation bit.
380// The hand-written Component fallback can replace an already prepared owner;
381// retain that previous adoption decision just as the old private marker did.
382#[derive(Clone, Copy)]
383enum HydrationOwnership {
384    Untracked,
385    CurrentOwner,
386    Preserved(bool),
387}
388impl HydrationOwnership {
389    fn value(self, owner: &Owner) -> Option<bool> {
390        match self {
391            Self::Untracked => None,
392            Self::CurrentOwner => Some(owner.was_activated()),
393            Self::Preserved(owned) => Some(owned),
394        }
395    }
396}
397
398impl Drop for Scope {
399    fn drop(&mut self) {
400        self.owner.dispose();
401        self.effects.clear();
402        self.listeners.clear();
403        self.children.clear();
404        self.retained.clear();
405        self.component_state.take();
406        let hydrated_owned = self.hydration_ownership.value(&self.owner);
407        if !self.hydrating || hydrated_owned == Some(true) {
408            if let Some(fragment) = &self.fragment {
409                fragment.remove();
410            }
411        }
412        if self.remove_on_drop && hydrated_owned.unwrap_or(true) {
413            remove_tree(&self.root);
414        }
415    }
416}
417
418impl crate::render::Scope for Scope {
419    fn owner(&self) -> OwnerHandle {
420        self.owner()
421    }
422
423    fn retain_state<T: 'static>(&mut self, state: T) -> Rc<T> {
424        self.retain_state(state)
425    }
426
427    fn prepares_effects(&self) -> bool {
428        self.prepares_effects()
429    }
430}
431
432impl Scope {
433    pub fn new(root: Element) -> Self {
434        let owner = Owner::new();
435        owner.commit();
436        Self::with_owner(root, owner)
437    }
438
439    // Generated mounts allocate their final owner before resolution, but only
440    // configure its integrations after the complete descriptor has validated.
441    fn new_prepared(root: Element, parent: Option<&OwnerHandle>) -> Self {
442        Self::with_owner(root, parent.map(Owner::child).unwrap_or_default())
443    }
444
445    fn with_owner(root: Element, owner: Owner) -> Self {
446        Self {
447            owner,
448            root,
449            fragment: None,
450            effects: Vec::new(),
451            listeners: Vec::new(),
452            children: Vec::new(),
453            component_state: None,
454            retained: Vec::new(),
455            mount_queue: None,
456            mount_parent: None,
457            remove_on_drop: false,
458            render_tree: None,
459            hydrating: false,
460            hydration_ownership: HydrationOwnership::Untracked,
461        }
462    }
463
464    /// Attach to existing, ordinary HTML.
465    pub fn at(selector: &str) -> Result<Self, JsValue> {
466        Ok(Self::new(
467            document()?
468                .query_selector(selector)?
469                .ok_or_else(|| missing(selector))?,
470        ))
471    }
472
473    /// Clone a standard HTML `<template>` containing exactly one root element.
474    /// Templates contain plain HTML; Rust wires up the cloned elements.
475    pub fn from_template(selector: &str) -> Result<Self, JsValue> {
476        let template = document()?
477            .query_selector(selector)?
478            .ok_or_else(|| missing(selector))?
479            .dyn_into::<HtmlTemplateElement>()
480            .map_err(|_| JsValue::from_str("fusor: expected an HTML template"))?;
481        Self::clone_template(&template)
482    }
483
484    fn clone_template(template: &HtmlTemplateElement) -> Result<Self, JsValue> {
485        Ok(Self::new(Self::clone_template_root(template)?))
486    }
487
488    fn clone_template_root(template: &HtmlTemplateElement) -> Result<Element, JsValue> {
489        let content = template.content();
490        if content.child_element_count() != 1 {
491            return Err(JsValue::from_str(
492                "fusor: a row template needs exactly one root element",
493            ));
494        }
495        Ok(content
496            .first_element_child()
497            .expect("one template element")
498            .clone_node_with_deep(true)?
499            .dyn_into::<Element>()?)
500    }
501
502    pub fn root(&self) -> &Element {
503        &self.root
504    }
505
506    pub fn owner(&self) -> OwnerHandle {
507        self.owner.handle()
508    }
509
510    /// Stop owned work immediately, even while an integration retains the scope.
511    pub fn dispose(&self) {
512        self.owner.dispose();
513    }
514
515    #[doc(hidden)]
516    pub fn is_hydrating(&self) -> bool {
517        self.hydrating
518    }
519
520    #[doc(hidden)]
521    pub fn prepares_effects(&self) -> bool {
522        #[cfg(feature = "islands")]
523        if delivery::enabled() {
524            return true;
525        }
526        self.is_coherent()
527    }
528
529    /// Activate prepared work after insertion succeeds. Ancestors must also commit.
530    /// Logs setup failure and disposes the owner. Use [`Self::try_commit`] when
531    /// the caller must propagate a setup error.
532    pub fn commit(&self) {
533        if let Err(error) = self.try_commit() {
534            self.owner.dispose();
535            web_sys::console::error_1(&error);
536        }
537    }
538
539    #[doc(hidden)]
540    pub fn prepare_owner(&mut self, parent: Option<&OwnerHandle>) {
541        // A fresh owner resets readiness and invalidates any previous queued setup.
542        self.owner = parent.map(Owner::child).unwrap_or_default();
543        self.finish_owner_preparation(parent);
544    }
545
546    fn finish_owner_preparation(&mut self, parent: Option<&OwnerHandle>) {
547        self.prepare_queue(parent);
548        self.prepare_coherent(parent);
549        #[cfg(feature = "islands")]
550        delivery::prepare_preview_owner(&self.owner(), parent);
551        if self.hydrating {
552            // This used to be the first activation callback on the fresh owner.
553            // Owner's monotone history records the same transition without a
554            // per-scope Rc, callback registry entry and retained registration.
555            self.hydration_ownership = HydrationOwnership::CurrentOwner;
556        }
557    }
558
559    /// Retain an integration guard for this scope without replacing component state.
560    pub fn retain(&mut self, guard: impl std::any::Any) {
561        self.retained.push(Box::new(guard));
562    }
563
564    /// Insert a prepared view. Its root will be removed on drop. Does not commit.
565    pub fn attach(&mut self, container: &Element) -> Result<(), JsValue> {
566        container.append_child(&self.root)?;
567        self.remove_on_drop = true;
568        Ok(())
569    }
570
571    /// Keep component state alive even when its template has no dynamic bindings.
572    #[doc(hidden)]
573    pub fn retain_state<C: 'static>(&mut self, value: C) -> Rc<C> {
574        let state = Rc::new(value);
575        self.component_state = Some(state.clone());
576        state
577    }
578
579    /// Adopt a component attached to existing markup, retaining its lifetime.
580    pub fn adopt(&mut self, child: Scope) {
581        child.commit();
582        self.children.push(child);
583    }
584
585    /// Append a component and adopt its lifetime. Dropping the parent detaches
586    /// its bindings and removes the mounted child's root from the document.
587    pub fn mount_child(
588        &mut self,
589        target: impl ElementTarget,
590        mut child: Scope,
591    ) -> Result<(), JsValue> {
592        target.resolve(self)?.append_child(&child.root)?;
593        child.remove_on_drop = true;
594        child.try_commit()?;
595        self.children.push(child);
596        Ok(())
597    }
598
599    /// Select within this island. `:scope` addresses the root itself.
600    pub fn select(&self, selector: &str) -> Result<Element, JsValue> {
601        if selector == ":scope" || self.root.matches(selector)? {
602            return Ok(self.root.clone());
603        }
604        self.root
605            .query_selector(selector)?
606            .ok_or_else(|| missing(selector))
607    }
608}
609
610#[cfg(test)]
611mod handler_tests {
612    use super::*;
613
614    fn handler() -> Handler {
615        Rc::new(|_| {})
616    }
617
618    #[test]
619    fn released_slots_reject_stale_generations_and_are_reused() {
620        let mut handlers = Handlers::default();
621        let first = handler();
622        let (slot, generation) = handlers.insert(first.clone());
623        assert!(Rc::ptr_eq(&handlers.get(slot, generation).unwrap(), &first));
624        assert!(handlers.get(slot, generation + 1).is_none());
625        assert!(Rc::ptr_eq(
626            &handlers.remove(slot, generation).unwrap(),
627            &first
628        ));
629        assert!(handlers.get(slot, generation).is_none());
630        assert!(handlers.remove(slot, generation).is_none());
631        let second = handler();
632        let (reused, next) = handlers.insert(second.clone());
633        assert_eq!(reused, slot);
634        assert_ne!(next, generation);
635        assert!(handlers.get(slot, generation).is_none(), "stale listener");
636        assert!(handlers.remove(slot, generation).is_none(), "stale removal");
637        assert!(Rc::ptr_eq(&handlers.get(slot, next).unwrap(), &second));
638        let (other, _) = handlers.insert(handler());
639        assert_ne!(other, slot);
640        assert!(handlers.get(99, 0).is_none());
641    }
642}