1pub 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
42pub 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 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 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 #[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 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#[doc(hidden)]
95pub type ComponentFactory<'a, C> = Box<dyn FnOnce(OwnerHandle) -> Result<C, JsValue> + 'a>;
96
97pub trait TemplateComponent: Component {}
100
101pub 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
135pub 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
150fn 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
168fn 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
191fn 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
203fn 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#[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 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 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
261enum ListenerTarget {
264 Node(EventTarget),
265 Bundle(Rc<JsValue>, u32),
266}
267
268pub struct Listener {
270 target: ListenerTarget,
271 event: strings::EventName,
272 slot: u32,
273 generation: u32,
274}
275
276impl Listener {
277 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 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#[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#[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 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 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 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 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 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 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 self.hydration_ownership = HydrationOwnership::CurrentOwner;
532 }
533 }
534
535 pub fn retain(&mut self, guard: impl std::any::Any) {
537 self.retained.push(Box::new(guard));
538 }
539
540 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 #[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 pub fn adopt(&mut self, child: Scope) {
557 child.commit();
558 self.children.push(child);
559 }
560
561 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 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}