bloom_client/
partial.rs

1//! Partial rendering support for bloom is WIP
2use async_channel::Sender;
3use bloom_core::{render_loop, Element, ObjectModel};
4use bloom_html::HtmlNode;
5use std::{
6    any::{Any, TypeId},
7    cell::RefCell,
8    collections::HashMap,
9    fmt::Debug,
10    sync::Arc,
11};
12use wasm_bindgen_futures::spawn_local;
13use web_sys::{console, js_sys::Array, window, Node};
14
15use crate::{dom::Dom, interned_str::interned, spawner::WasmSpawner};
16
17#[derive(Default)]
18struct PartialRenderingContext {
19    context: Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
20    subscribers: Vec<Sender<()>>,
21}
22
23impl Drop for PartialRenderingContext {
24    fn drop(&mut self) {
25        for subscriber in self.subscribers.drain(..) {
26            subscriber.close();
27        }
28    }
29}
30
31thread_local! {
32    static CONTEXT: RefCell<HashMap<u64, PartialRenderingContext>> = RefCell::new(HashMap::new());
33}
34
35struct PartialDom(Dom, u64);
36
37impl PartialDom {
38    fn hydrate_from(
39        context_id: u64,
40        root: Arc<HtmlNode>,
41        dom_node: Node,
42        start_index: i32,
43    ) -> Self {
44        let mut inner = Dom::hydrate();
45        inner.register(&root, dom_node);
46        inner.set_hydration_index(root, start_index.unsigned_abs());
47        Self(inner, context_id)
48    }
49}
50
51impl ObjectModel for PartialDom {
52    type Node = HtmlNode;
53
54    fn create(
55        &mut self,
56        node: &Arc<Self::Node>,
57        parent: &Arc<Self::Node>,
58        sibling: &Option<Arc<Self::Node>>,
59    ) {
60        self.0.create(node, parent, sibling)
61    }
62
63    fn update(&mut self, node: &Arc<Self::Node>, next: &Arc<Self::Node>) {
64        self.0.update(node, next)
65    }
66
67    fn remove(&mut self, node: &Arc<Self::Node>, parent: &Arc<Self::Node>) {
68        self.0.remove(node, parent)
69    }
70
71    fn finalize(&mut self) -> impl futures_util::Future<Output = ()> + Send {
72        self.0.finalize()
73    }
74
75    fn subscribe(&mut self, signal: Sender<()>) {
76        CONTEXT.with(|context| {
77            let mut context = context.borrow_mut();
78            let context = context
79                .entry(self.1)
80                .or_insert_with(|| PartialRenderingContext::default());
81            context.subscribers.push(signal);
82        });
83    }
84
85    fn get_context(&mut self) -> Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>> {
86        CONTEXT.with(|context| {
87            let context = context.borrow();
88            let context = context.get(&self.1).unwrap();
89            Arc::clone(&context.context)
90        })
91    }
92}
93
94pub fn hydrate_partial<E>(partial_id: String, element: Element<HtmlNode, E>)
95where
96    E: Send + 'static + Debug,
97{
98    spawn_local(async {
99        let first_node = if let Some(first_node) = window()
100            .expect("Failed to get Window")
101            .document()
102            .expect("Failed to get Document")
103            .query_selector(&format!("[data-bloom-partial='{}']", partial_id))
104            .expect("Failed to query selector for partial")
105        {
106            first_node
107        } else {
108            console::warn_2(&"Failed to find Partial Element".into(), &partial_id.into());
109            return;
110        };
111
112        let root_dom_node = first_node
113            .parent_element()
114            .expect("Failed to get Parent for Partial Hydration");
115
116        let root: Arc<HtmlNode> = Arc::new(
117            HtmlNode::element(interned(root_dom_node.tag_name().to_lowercase()))
118                .build()
119                .into(),
120        );
121        let start_index = Array::from(&root_dom_node.child_nodes()).index_of(&first_node, 0);
122        let context_id = u64::from_str_radix(
123            &first_node
124                .get_attribute("data-bloom-ctx")
125                .expect("Failed to get attribute"),
126            16,
127        )
128        .expect("Failed to parse context id");
129
130        let dom =
131            PartialDom::hydrate_from(context_id, root.clone(), root_dom_node.into(), start_index);
132
133        if let Err(error) = render_loop(root, element, WasmSpawner, dom).await {
134            let msg = format!("Render loop error: {:?}", error);
135            console::error_1(&msg.into());
136        }
137    })
138}