Skip to main content

fusor/dom/
content.rs

1//! Lazy, typed HTML content. Factories are reusable; mounted scopes are unique.
2use super::{ElementTarget, JsValue, Scope, TemplateComponent, component::Retained};
3use crate::{OwnerHandle, untrack};
4use std::rc::Rc;
5
6type Factory = dyn Fn(&OwnerHandle) -> Result<Scope, JsValue>;
7
8/// A reusable factory for an authored HTML template component.
9///
10/// Clone shares factory identity, never DOM or a mounted lifetime. Each slot
11/// prepares a fresh child of its receiving component; captured Rust values keep
12/// their ordinary lexical scope. Construct content once and pass clones to slots.
13/// Making a new factory in a reactive slot expression deliberately replaces it.
14#[derive(Clone)]
15#[must_use = "content is lazy; supply it to a slot to mount it"]
16pub struct Content(Rc<Factory>);
17
18impl Content {
19    pub fn new<C: TemplateComponent>(make: impl Fn(OwnerHandle) -> C + 'static) -> Self {
20        Self::try_new(move |owner| Ok(make(owner)))
21    }
22
23    /// Fallible constructor; work waits for the receiving scope's commit.
24    pub fn try_new<C: TemplateComponent>(
25        make: impl Fn(OwnerHandle) -> Result<C, JsValue> + 'static,
26    ) -> Self {
27        Self(Rc::new(move |parent| C::prepare(parent, &make)))
28    }
29
30    /// Compiler contract for inline content with an inferred parent state type.
31    /// The normal detached-child checks still run in `prepare`.
32    #[doc(hidden)]
33    pub fn from_prepared(make: impl Fn(&OwnerHandle) -> Result<Scope, JsValue> + 'static) -> Self {
34        Self(Rc::new(make))
35    }
36
37    /// Prepare a fresh, detached template child for an outlet or custom host.
38    /// The caller attaches it and commits its scope; owned work waits for commit.
39    pub fn prepare(&self, parent: &OwnerHandle) -> Result<Scope, JsValue> {
40        let scope = (self.0)(parent)?;
41        let owner = scope.owner();
42        if owner.is_disposed()
43            || owner.is_active()
44            || !owner.is_child_of(parent)
45            || scope.root().parent_node().is_some()
46        {
47            return Err(JsValue::from_str(
48                "fusor: slot content must be a detached, prepared child of its receiving owner",
49            ));
50        }
51        Ok(scope)
52    }
53}
54
55impl PartialEq for Content {
56    fn eq(&self, other: &Self) -> bool {
57        Rc::ptr_eq(&self.0, &other.0)
58    }
59}
60
61impl Eq for Content {}
62
63impl Scope {
64    /// Mount optional content in an empty host. An unchanged factory retains its
65    /// DOM and local state; `None` removes it. Read signals inside `read` to select
66    /// content reactively. Each host owns an independent instance.
67    pub fn slot<R: Into<Option<Content>>>(
68        &mut self,
69        target: impl ElementTarget,
70        read: impl Fn() -> R + 'static,
71    ) -> Result<(), JsValue> {
72        self.slot_with(target, move || read().into().map(|content| ((), content)))
73    }
74
75    /// Like [`Self::slot`], with an explicit reset key in addition to factory
76    /// identity. Failed preparation leaves the previous instance alive.
77    pub fn slot_with<K: PartialEq + 'static>(
78        &mut self,
79        target: impl ElementTarget,
80        read: impl Fn() -> Option<(K, Content)> + 'static,
81    ) -> Result<(), JsValue> {
82        let container = target.resolve(self)?;
83        let parent = self.owner();
84        let mut current: Retained<(K, Content)> = Retained::default();
85        self.bind(move || {
86            let next = read();
87            untrack(|| {
88                let Some(identity) = next else {
89                    current.clear();
90                    return Ok(());
91                };
92                if current.key() == Some(&identity) {
93                    return Ok(());
94                }
95                let child = identity.1.prepare(&parent)?;
96                current.replace(identity, child, |child| child.attach(&container))
97            })
98        })
99    }
100}