bloom-client 0.1.0

Client-side rendering for bloom-core
Documentation
//! Partial rendering support for bloom is WIP
use async_channel::Sender;
use bloom_core::{render_loop, Element, ObjectModel};
use bloom_html::HtmlNode;
use std::{
    any::{Any, TypeId},
    cell::RefCell,
    collections::HashMap,
    fmt::Debug,
    sync::Arc,
};
use wasm_bindgen_futures::spawn_local;
use web_sys::{console, js_sys::Array, window, Node};

use crate::{dom::Dom, interned_str::interned, spawner::WasmSpawner};

#[derive(Default)]
struct PartialRenderingContext {
    context: Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
    subscribers: Vec<Sender<()>>,
}

impl Drop for PartialRenderingContext {
    fn drop(&mut self) {
        for subscriber in self.subscribers.drain(..) {
            subscriber.close();
        }
    }
}

thread_local! {
    static CONTEXT: RefCell<HashMap<u64, PartialRenderingContext>> = RefCell::new(HashMap::new());
}

struct PartialDom(Dom, u64);

impl PartialDom {
    fn hydrate_from(
        context_id: u64,
        root: Arc<HtmlNode>,
        dom_node: Node,
        start_index: i32,
    ) -> Self {
        let mut inner = Dom::hydrate();
        inner.register(&root, dom_node);
        inner.set_hydration_index(root, start_index.unsigned_abs());
        Self(inner, context_id)
    }
}

impl ObjectModel for PartialDom {
    type Node = HtmlNode;

    fn create(
        &mut self,
        node: &Arc<Self::Node>,
        parent: &Arc<Self::Node>,
        sibling: &Option<Arc<Self::Node>>,
    ) {
        self.0.create(node, parent, sibling)
    }

    fn update(&mut self, node: &Arc<Self::Node>, next: &Arc<Self::Node>) {
        self.0.update(node, next)
    }

    fn remove(&mut self, node: &Arc<Self::Node>, parent: &Arc<Self::Node>) {
        self.0.remove(node, parent)
    }

    fn finalize(&mut self) -> impl futures_util::Future<Output = ()> + Send {
        self.0.finalize()
    }

    fn subscribe(&mut self, signal: Sender<()>) {
        CONTEXT.with(|context| {
            let mut context = context.borrow_mut();
            let context = context
                .entry(self.1)
                .or_insert_with(|| PartialRenderingContext::default());
            context.subscribers.push(signal);
        });
    }

    fn get_context(&mut self) -> Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>> {
        CONTEXT.with(|context| {
            let context = context.borrow();
            let context = context.get(&self.1).unwrap();
            Arc::clone(&context.context)
        })
    }
}

pub fn hydrate_partial<E>(partial_id: String, element: Element<HtmlNode, E>)
where
    E: Send + 'static + Debug,
{
    spawn_local(async {
        let first_node = if let Some(first_node) = window()
            .expect("Failed to get Window")
            .document()
            .expect("Failed to get Document")
            .query_selector(&format!("[data-bloom-partial='{}']", partial_id))
            .expect("Failed to query selector for partial")
        {
            first_node
        } else {
            console::warn_2(&"Failed to find Partial Element".into(), &partial_id.into());
            return;
        };

        let root_dom_node = first_node
            .parent_element()
            .expect("Failed to get Parent for Partial Hydration");

        let root: Arc<HtmlNode> = Arc::new(
            HtmlNode::element(interned(root_dom_node.tag_name().to_lowercase()))
                .build()
                .into(),
        );
        let start_index = Array::from(&root_dom_node.child_nodes()).index_of(&first_node, 0);
        let context_id = u64::from_str_radix(
            &first_node
                .get_attribute("data-bloom-ctx")
                .expect("Failed to get attribute"),
            16,
        )
        .expect("Failed to parse context id");

        let dom =
            PartialDom::hydrate_from(context_id, root.clone(), root_dom_node.into(), start_index);

        if let Err(error) = render_loop(root, element, WasmSpawner, dom).await {
            let msg = format!("Render loop error: {:?}", error);
            console::error_1(&msg.into());
        }
    })
}