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