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; owner-activation registrations wait for the receiving
24    /// scope's commit. Ordinary constructor effects retain their immediate timing.
25    pub fn try_new<C: TemplateComponent>(
26        make: impl Fn(OwnerHandle) -> Result<C, JsValue> + 'static,
27    ) -> Self {
28        Self(Rc::new(move |parent| C::prepare(parent, &make)))
29    }
30
31    /// Compiler contract for inline content with an inferred parent state type.
32    /// The normal detached-child checks still run in `prepare`.
33    #[doc(hidden)]
34    pub fn from_prepared(make: impl Fn(&OwnerHandle) -> Result<Scope, JsValue> + 'static) -> Self {
35        Self(Rc::new(make))
36    }
37
38    /// Prepare a fresh, detached template child for an outlet or custom host.
39    /// The caller attaches it and commits its scope; activation registrations wait
40    /// for commit. Ordinary constructor effects retain their immediate timing.
41    pub fn prepare(&self, parent: &OwnerHandle) -> Result<Scope, JsValue> {
42        let scope = (self.0)(parent)?;
43        let owner = scope.owner();
44        if owner.is_disposed()
45            || owner.is_active()
46            || !owner.is_child_of(parent)
47            || scope.root().parent_node().is_some()
48        {
49            return Err(JsValue::from_str(
50                "fusor: slot content must be a detached, prepared child of its receiving owner",
51            ));
52        }
53        Ok(scope)
54    }
55}
56
57impl PartialEq for Content {
58    fn eq(&self, other: &Self) -> bool {
59        Rc::ptr_eq(&self.0, &other.0)
60    }
61}
62
63impl Eq for Content {}
64
65impl Scope {
66    /// Mount optional content in an empty host. An unchanged factory retains its
67    /// DOM and local state; `None` removes it. Read signals inside `read` to select
68    /// content reactively. Each host owns an independent instance.
69    pub fn slot<R: Into<Option<Content>>>(
70        &mut self,
71        target: impl ElementTarget,
72        read: impl Fn() -> R + 'static,
73    ) -> Result<(), JsValue> {
74        self.slot_with(target, move || read().into().map(|content| ((), content)))
75    }
76
77    /// Like [`Self::slot`], with an explicit reset key in addition to factory
78    /// identity. Failed preparation leaves the previous instance alive.
79    pub fn slot_with<K: PartialEq + 'static>(
80        &mut self,
81        target: impl ElementTarget,
82        read: impl Fn() -> Option<(K, Content)> + 'static,
83    ) -> Result<(), JsValue> {
84        let container = target.resolve(self)?;
85        let parent = self.owner();
86        let mut current: Retained<(K, Content)> = Retained::default();
87        self.bind(move || {
88            let next = read();
89            untrack(|| {
90                let Some(identity) = next else {
91                    current.clear();
92                    return Ok(());
93                };
94                if current.key() == Some(&identity) {
95                    return Ok(());
96                }
97                let child = identity.1.prepare(&parent)?;
98                current.replace(identity, child, |child| child.attach(&container))
99            })
100        })
101    }
102}