use std::cell::{LazyCell, RefCell};
use std::collections::HashMap;
use super::dom_node::DomNode;
use super::*;
pub struct HydrateNode(NodeState);
enum NodeState {
Hydrated(DomNode),
TextDynamic(DomNode),
TextStatic,
Marker(DomNode),
}
impl NodeState {
#[cfg_attr(debug_assertions, track_caller)]
fn unwrap(self) -> DomNode {
match self {
Self::Hydrated(node) | Self::TextDynamic(node) | Self::Marker(node) => node,
_ => panic!("node is not hydrated"),
}
}
#[cfg_attr(debug_assertions, track_caller)]
fn unwrap_ref(&self) -> &DomNode {
match self {
Self::Hydrated(node) | Self::TextDynamic(node) | Self::Marker(node) => node,
_ => panic!("node is not hydrated"),
}
}
#[cfg_attr(debug_assertions, track_caller)]
fn unwrap_mut(&mut self) -> &mut DomNode {
match self {
Self::Hydrated(node) | Self::TextDynamic(node) | Self::Marker(node) => node,
_ => panic!("node is not hydrated"),
}
}
}
impl From<HydrateNode> for View<HydrateNode> {
fn from(node: HydrateNode) -> Self {
View::from_node(node)
}
}
impl ViewNode for HydrateNode {
fn append_child(&mut self, child: Self) {
if IS_HYDRATING.get() {
match child.0 {
NodeState::Hydrated(_) => {
}
NodeState::TextDynamic(node) => {
console_warn!("text dynamic");
let mut next = self.as_web_sys().first_child();
while let Some(current) = next {
if current.node_type() == web_sys::Node::COMMENT_NODE {
let comment = current.unchecked_ref::<web_sys::Comment>();
if comment.text_content().unwrap() == "t" {
let text_node = comment.next_sibling().unwrap();
self.as_web_sys()
.replace_child(&node.as_web_sys(), &text_node)
.unwrap();
self.as_web_sys().remove_child(&comment).unwrap();
return;
}
}
next = current.next_sibling();
}
panic!("text node not found during hydration");
}
NodeState::TextStatic => {
}
NodeState::Marker(node) => {
let mut next = self.as_web_sys().first_child();
while let Some(current) = next {
if current.node_type() == web_sys::Node::COMMENT_NODE {
let comment = current.unchecked_ref::<web_sys::Comment>();
if comment.text_content().unwrap() == "/" {
self.as_web_sys()
.replace_child(&node.as_web_sys(), ¤t)
.unwrap();
node.as_web_sys().set_text_content(Some("#"));
return;
}
}
next = current.next_sibling();
}
panic!("hydration marker node not found");
}
}
} else {
self.0.unwrap_mut().append_child(child.0.unwrap());
}
}
fn create_dynamic_view<U: Into<View<Self>> + 'static>(
f: impl FnMut() -> U + 'static,
) -> View<Self> {
_create_dynamic_view(f)
}
}
fn check_node(node: &web_sys::Node, tag: &str, key: HydrationKey) {
if cfg!(debug_assertions) {
let node = node.unchecked_ref::<web_sys::Element>();
node.set_attribute("data-hydrated", "").unwrap();
assert_eq!(node.node_type(), web_sys::Node::ELEMENT_NODE);
assert_eq!(node.get_attribute("data-hk"), Some(key.to_string()));
let actual_tag = node.tag_name().to_lowercase();
if actual_tag != tag {
console_warn!(
"hydration tag name mismatch at `{key}`: expected '{tag}', got '{actual_tag}'"
);
}
}
}
impl ViewHtmlNode for HydrateNode {
fn create_element(tag: Cow<'static, str>) -> Self {
if IS_HYDRATING.get() {
let reg: HydrationRegistry = use_context();
let key = reg.next_key();
let node = HYDRATE_NODES
.with(|nodes| nodes.borrow_mut().remove(&key))
.unwrap_or_else(|| panic!("node with hk `{key}` not found"));
check_node(node.as_web_sys(), &tag, key);
node
} else {
Self(NodeState::Hydrated(DomNode::create_element(tag)))
}
}
fn create_element_ns(namespace: &'static str, tag: Cow<'static, str>) -> Self {
if IS_HYDRATING.get() {
let reg: HydrationRegistry = use_context();
let key = reg.next_key();
let node = HYDRATE_NODES
.with(|nodes| nodes.borrow_mut().remove(&key))
.unwrap_or_else(|| panic!("node with hk `{key}` not found"));
if cfg!(debug_assertions) {
node.as_web_sys()
.unchecked_ref::<web_sys::Element>()
.set_attribute("data-hydrated", "")
.unwrap();
}
node
} else {
Self(NodeState::Hydrated(DomNode::create_element_ns(
namespace, tag,
)))
}
}
fn create_text_node(text: Cow<'static, str>) -> Self {
if IS_HYDRATING.get() {
Self(NodeState::TextStatic)
} else {
Self(NodeState::Hydrated(DomNode::create_text_node(text)))
}
}
fn create_dynamic_text_node(text: Cow<'static, str>) -> Self {
if IS_HYDRATING.get() {
Self(NodeState::TextDynamic(DomNode::create_text_node(text)))
} else {
Self(NodeState::Hydrated(DomNode::create_text_node(text)))
}
}
fn create_marker_node() -> Self {
Self(NodeState::Marker(DomNode::create_marker_node()))
}
fn set_attribute(&mut self, name: Cow<'static, str>, value: StringAttribute) {
if IS_HYDRATING.get() {
if value.as_static().is_none() {
let node = self
.as_web_sys()
.clone()
.unchecked_into::<web_sys::Element>();
create_effect_initial(move || {
let _ = value.track(); (
Box::new(move || match value.get_clone() {
Some(value) => node.set_attribute(&name, &value).unwrap(),
None => node.remove_attribute(&name).unwrap(),
}),
(),
)
});
}
} else {
self.0.unwrap_mut().set_attribute(name, value);
}
}
fn set_bool_attribute(&mut self, name: Cow<'static, str>, value: BoolAttribute) {
if IS_HYDRATING.get() {
if value.as_static().is_none() {
let node = self
.as_web_sys()
.clone()
.unchecked_into::<web_sys::Element>();
create_effect_initial(move || {
let _ = value.track(); (
Box::new(move || {
if value.get() {
node.set_attribute(&name, "").unwrap();
} else {
node.remove_attribute(&name).unwrap();
}
}),
(),
)
});
}
} else {
self.0.unwrap_mut().set_bool_attribute(name, value);
}
}
fn set_property(&mut self, name: Cow<'static, str>, value: MaybeDyn<JsValue>) {
self.0.unwrap_mut().set_property(name, value);
}
fn set_event_handler(
&mut self,
name: Cow<'static, str>,
handler: impl FnMut(web_sys::Event) + 'static,
) {
self.0.unwrap_mut().set_event_handler(name, handler);
}
fn set_inner_html(&mut self, inner_html: Cow<'static, str>) {
if !IS_HYDRATING.get() {
self.0.unwrap_mut().set_inner_html(inner_html);
}
}
#[cfg_attr(debug_assertions, track_caller)]
fn as_web_sys(&self) -> &web_sys::Node {
self.0.unwrap_ref().as_web_sys()
}
fn from_web_sys(node: web_sys::Node) -> Self {
Self(NodeState::Hydrated(DomNode::from_web_sys(node)))
}
}
thread_local! {
pub(crate) static HYDRATE_NODES: LazyCell<RefCell<HashMap<HydrationKey, HydrateNode>>> = LazyCell::new(Default::default);
}