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