pub mod application;
mod bindings;
mod branch;
mod children;
pub mod coherent;
mod commit;
mod component;
mod content;
#[doc(hidden)]
pub use children::Children;
#[cfg(feature = "islands")]
pub mod delivery;
mod keyed;
mod mount;
mod property;
mod reconcile;
mod strings;
mod target;
#[doc(hidden)]
pub mod text_value;
#[doc(hidden)]
pub use component::{MountGuard, MountPoint};
pub use content::Content;
#[doc(hidden)]
pub use mount::TemplateNodes;
pub use target::{ElementTarget, InputTarget};
use crate::{Effect, Owner, OwnerHandle};
use std::{cell::Cell, rc::Rc};
pub use wasm_bindgen::JsValue;
use wasm_bindgen::{JsCast, closure::Closure};
use web_sys::{Document, Element, Event, EventTarget, HtmlTemplateElement};
pub trait Component: Sized + 'static {
const TEMPLATE_HASH: &'static str = "";
const TEMPLATE_HTML: &'static str = "";
fn mount(self) -> Result<Scope, JsValue>;
fn mount_with(make: impl FnOnce(OwnerHandle) -> Self) -> Result<Scope, JsValue> {
Self::try_mount_with(|owner| Ok(make(owner)))
}
fn try_mount_with(
make: impl FnOnce(OwnerHandle) -> Result<Self, JsValue>,
) -> Result<Scope, JsValue> {
let scope = Self::prepare_component(None, Box::new(make))?;
scope.try_commit()?;
Ok(scope)
}
fn prepare(
parent: &OwnerHandle,
make: impl FnOnce(OwnerHandle) -> Result<Self, JsValue>,
) -> Result<Scope, JsValue> {
Self::prepare_component(Some(parent), Box::new(make))
}
#[doc(hidden)]
fn prepare_component(
parent: Option<&OwnerHandle>,
make: ComponentFactory<'_, Self>,
) -> Result<Scope, JsValue> {
let owner = parent.map(Owner::child).unwrap_or_default();
let mut scope = make(owner.handle())?.mount()?;
if let Some(owned) = scope.hydration_ownership.value(&scope.owner) {
scope.hydration_ownership = HydrationOwnership::Preserved(owned);
}
scope.owner = owner;
scope.mount_ready = Rc::new(Cell::new(false));
scope.prepare_queue(parent);
Ok(scope)
}
}
#[doc(hidden)]
pub type ComponentFactory<'a, C> = Box<dyn FnOnce(OwnerHandle) -> Result<C, JsValue> + 'a>;
pub trait TemplateComponent: Component {}
pub trait FromInputs: Sized {
type Inputs;
fn from_inputs(inputs: Self::Inputs, owner: OwnerHandle) -> Result<Self, JsValue>;
}
pub fn document() -> Result<Document, JsValue> {
web_sys::window()
.and_then(|window| window.document())
.ok_or_else(|| JsValue::from_str("fusor requires a browser document"))
}
pub fn element(tag: &str) -> Result<Element, JsValue> {
document()?.create_element(tag)
}
fn missing(selector: &str) -> JsValue {
JsValue::from_str(&format!("fusor: no element matches {selector:?}"))
}
struct Listener {
target: EventTarget,
event: strings::EventName,
callback: Closure<dyn Fn(Event)>,
}
impl Drop for Listener {
fn drop(&mut self) {
let _ = strings::remove(&self.target, &self.event, self.callback.as_ref());
}
}
#[must_use = "retain the scope to keep its DOM bindings and event listeners active"]
pub struct Scope {
owner: Owner,
root: Element,
fragment: Option<MountPoint>,
effects: Vec<Effect>,
listeners: Vec<Listener>,
children: Vec<Scope>,
component_state: Option<Rc<dyn std::any::Any>>,
retained: Vec<Box<dyn std::any::Any>>,
mount_queue: Option<Rc<commit::CommitQueue>>,
mount_parent: Option<OwnerHandle>,
mount_ready: Rc<Cell<bool>>,
remove_on_drop: bool,
render_tree: Option<Rc<coherent::Tree>>,
hydrating: bool,
hydration_ownership: HydrationOwnership,
}
#[derive(Clone, Copy)]
enum HydrationOwnership {
Untracked,
CurrentOwner,
Preserved(bool),
}
impl HydrationOwnership {
fn value(self, owner: &Owner) -> Option<bool> {
match self {
Self::Untracked => None,
Self::CurrentOwner => Some(owner.was_activated()),
Self::Preserved(owned) => Some(owned),
}
}
}
impl Drop for Scope {
fn drop(&mut self) {
self.owner.dispose();
self.effects.clear();
self.listeners.clear();
self.children.clear();
self.retained.clear();
self.component_state.take();
let hydrated_owned = self.hydration_ownership.value(&self.owner);
if !self.hydrating || hydrated_owned == Some(true) {
if let Some(fragment) = &self.fragment {
fragment.remove();
}
}
if self.remove_on_drop && hydrated_owned.unwrap_or(true) {
#[cfg(feature = "islands")]
delivery::dispose_tree(&self.root);
self.root.remove();
}
}
}
impl Scope {
pub fn new(root: Element) -> Self {
let owner = Owner::new();
owner.commit();
Self::with_owner(root, owner)
}
fn new_prepared(root: Element, parent: Option<&OwnerHandle>) -> Self {
Self::with_owner(root, parent.map(Owner::child).unwrap_or_default())
}
fn with_owner(root: Element, owner: Owner) -> Self {
Self {
owner,
root,
fragment: None,
effects: Vec::new(),
listeners: Vec::new(),
children: Vec::new(),
component_state: None,
retained: Vec::new(),
mount_queue: None,
mount_parent: None,
mount_ready: Rc::new(Cell::new(false)),
remove_on_drop: false,
render_tree: None,
hydrating: false,
hydration_ownership: HydrationOwnership::Untracked,
}
}
pub fn at(selector: &str) -> Result<Self, JsValue> {
Ok(Self::new(
document()?
.query_selector(selector)?
.ok_or_else(|| missing(selector))?,
))
}
pub fn from_template(selector: &str) -> Result<Self, JsValue> {
let template = document()?
.query_selector(selector)?
.ok_or_else(|| missing(selector))?
.dyn_into::<HtmlTemplateElement>()
.map_err(|_| JsValue::from_str("fusor: expected an HTML template"))?;
Self::clone_template(&template)
}
fn clone_template(template: &HtmlTemplateElement) -> Result<Self, JsValue> {
Ok(Self::new(Self::clone_template_root(template)?))
}
fn clone_template_root(template: &HtmlTemplateElement) -> Result<Element, JsValue> {
let content = template.content();
if content.child_element_count() != 1 {
return Err(JsValue::from_str(
"fusor: a row template needs exactly one root element",
));
}
Ok(content
.first_element_child()
.expect("one template element")
.clone_node_with_deep(true)?
.dyn_into::<Element>()?)
}
pub fn root(&self) -> &Element {
&self.root
}
pub fn owner(&self) -> OwnerHandle {
self.owner.handle()
}
pub fn dispose(&self) {
self.owner.dispose();
}
#[doc(hidden)]
pub fn is_hydrating(&self) -> bool {
self.hydrating
}
#[doc(hidden)]
pub fn prepares_effects(&self) -> bool {
#[cfg(feature = "islands")]
if delivery::enabled() {
return true;
}
self.is_coherent()
}
pub fn commit(&self) {
if let Err(error) = self.try_commit() {
self.owner.dispose();
web_sys::console::error_1(&error);
}
}
#[doc(hidden)]
pub fn prepare_owner(&mut self, parent: Option<&OwnerHandle>) {
self.owner = parent.map(Owner::child).unwrap_or_default();
self.mount_ready = Rc::new(Cell::new(false));
self.finish_owner_preparation(parent);
}
fn finish_owner_preparation(&mut self, parent: Option<&OwnerHandle>) {
self.prepare_queue(parent);
self.prepare_coherent(parent);
#[cfg(feature = "islands")]
delivery::prepare_preview_owner(&self.owner(), parent);
if self.hydrating {
self.hydration_ownership = HydrationOwnership::CurrentOwner;
}
}
pub fn retain(&mut self, guard: impl std::any::Any) {
self.retained.push(Box::new(guard));
}
pub fn attach(&mut self, container: &Element) -> Result<(), JsValue> {
container.append_child(&self.root)?;
self.remove_on_drop = true;
Ok(())
}
#[doc(hidden)]
pub fn retain_state<C: 'static>(&mut self, value: C) -> Rc<C> {
let state = Rc::new(value);
self.component_state = Some(state.clone());
state
}
pub fn adopt(&mut self, child: Scope) {
child.commit();
self.children.push(child);
}
pub fn mount_child(
&mut self,
target: impl ElementTarget,
mut child: Scope,
) -> Result<(), JsValue> {
target.resolve(self)?.append_child(&child.root)?;
child.remove_on_drop = true;
child.try_commit()?;
self.children.push(child);
Ok(())
}
pub fn select(&self, selector: &str) -> Result<Element, JsValue> {
if selector == ":scope" || self.root.matches(selector)? {
return Ok(self.root.clone());
}
self.root
.query_selector(selector)?
.ok_or_else(|| missing(selector))
}
}