Picking away at hydration bugs

This commit is contained in:
Greg Johnston 2022-09-07 22:21:22 -04:00
parent 293bece526
commit 4a8339df18
10 changed files with 201 additions and 56 deletions

View file

@ -5,21 +5,21 @@ pub fn Nav(cx: Scope) -> Element {
view! {
<header class="header">
<nav class="inner">
<a href="/"> // <Link to="/".into()>
<Link to="/".into()>
<strong>"HN"</strong>
</a> // </Link>
<a href="/new"> // <Link to="/new".into()>
</Link>
<Link to="/new".into()>
<strong>"New"</strong>
</a> // </Link>
<a href="/show"> // <Link to="/show".into()>
</Link>
<Link to="/show".into()>
<strong>"Show"</strong>
</a> // </Link>
<a href="/ask"> // <Link to="/ask".into()>
</Link>
<Link to="/ask".into()>
<strong>"Ask"</strong>
</a> // </Link>
<a href="/job"> // <Link to="/job".into()>
</Link>
<Link to="/job".into()>
<strong>"Jobs"</strong>
</a> // </Link>
</Link>
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
"Built with Leptos"
</a>

View file

@ -65,6 +65,7 @@ pub fn Stories(cx: Scope) -> Element {
view! {
<div class="news-view">
<div class="news-list-nav">
// TODO fix
/* {move || if page() > 1 {
view! {
//<Link
@ -86,13 +87,13 @@ pub fn Stories(cx: Scope) -> Element {
{
move || if stories.read().unwrap_or(Err(())).unwrap_or_default().len() >= 28 {
view! {
//<Link
<Link
//attr:class="page-link"
//to={format!("/{}?page={}", story_type(), page() + 1)}
to={format!("/{}?page={}", story_type(), page() + 1)}
//attr:aria_label="Next Page"
<a href="#"> // href={format!("/{}?page={}", story_type(), page() + 1)}>
>
"more >"
</a> //</Link>
</Link>
}
} else {
view! {
@ -104,6 +105,13 @@ pub fn Stories(cx: Scope) -> Element {
}
</div>
<main class="news-list">
<p>"Stories" {move || match stories.read() {
None => "none".to_string(),
Some(s) => match s {
Err(_) => "err".to_string(),
Ok(stories) => stories.len().to_string()
}
}}</p>
{move || match stories.read() {
None => None,
Some(Err(_)) => Some(view! { <p>"Error loading stories."</p> }),
@ -113,10 +121,12 @@ pub fn Stories(cx: Scope) -> Element {
<ul>
<p>{stories.len()}</p>
<For each={move || stories.clone()} key=|story| story.id>{
|cx: Scope, story: &api::Story| view! {
<p>"Story"</p>
//<Story story={story.clone()} />
}
move |cx: Scope, story: &api::Story| {
log::debug!("Story entry {:?}", story);
view! {
//<p>"Story"</p>
<Story story={story.clone()} />
}}
}</For>
</ul>
})
@ -144,8 +154,7 @@ fn Story(cx: Scope, story: api::Story) -> Element {
}
} else {
let title = story.title.clone();
//view! { <Link to={format!("/stories/{}", story.id)}>{title}</Link> }
view! { <a href=format!("/stories/{}", story.id)>{title}</a>}
view! { <Link to={format!("/stories/{}", story.id)}>{title}</Link> }
}}
</span>
<br />
@ -153,27 +162,26 @@ fn Story(cx: Scope, story: api::Story) -> Element {
{if story.story_type != "job" {
view! {
<span>
{"by "}
{story.user.map(|user| view ! { <Link to={format!("/users/{}", user)}>{user}</Link>})}
{format!(" {} | ", story.time_ago)}
/* <Link to={format!("/stories/{}", story.id)}>
//{"by "}
//{story.user.map(|user| view ! { <Link to={format!("/users/{}", user)}>{&user}</Link>})}
//{format!(" {} | ", story.time_ago)}
<Link to={format!("/stories/{}", story.id)}>
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"discuss".into()
}}
</Link> */
</Link>
</span>
}
} else {
let title = story.title.clone();
//view! { <Link to={format!("/item/{}", story.id)}>{title}</Link> }
view! { <a href=format!("/item/{}", story.id)>{title}</a> }
view! { <Link to={format!("/item/{}", story.id)}>{title}</Link> }
}}
</span>
{(story.story_type != "link").then(|| view! {
<span>
{" "}
//{" "}
<span class="label">{story.story_type}</span>
</span>
})}

View file

@ -35,7 +35,7 @@ pub fn Story(cx: Scope) -> Element {
// TODO issue here in renderer
{story.points}
" points | by "
<Link to=format!("/users/{}", user)>{user}</Link>
<Link to=format!("/users/{}", user)>{&user}</Link>
{format!(" {}", story.time_ago)}
</p>})}
</div>
@ -66,7 +66,7 @@ pub fn Comment(cx: Scope, comment: api::Comment) -> Element {
view! {
<li class="comment">
<div class="by">
<Link to={format!("/users/{}", comment.user)}>{comment.user}</Link>
<Link to={format!("/users/{}", comment.user)}>{&comment.user}</Link>
{format!(" {}", comment.time_ago)}
</div>
<div class="text" inner_html={comment.content}></div>

View file

@ -9,8 +9,9 @@ leptos_dom = { path = "../leptos_dom" }
leptos_macro = { path = "../leptos_macro" }
leptos_reactive = { path = "../leptos_reactive" }
leptos_router = { path = "../router" }
leptos_ssr= { path = "../ssr" }
[features]
csr = ["leptos_core/csr", "leptos_router/csr", "leptos_macro/csr", "leptos_reactive/csr", "leptos_router/csr"]
hydrate = ["leptos_core/hydrate", "leptos_router/hydrate", "leptos_macro/hydrate", "leptos_reactive/hydrate", "leptos_router/hydrate"]
ssr = ["leptos_core/ssr", "leptos_router/ssr", "leptos_macro/ssr", "leptos_reactive/ssr", "leptos_router/ssr"]
csr = ["leptos_core/csr", "leptos_router/csr", "leptos_macro/csr", "leptos_reactive/csr", "leptos_router/csr", "leptos_ssr/csr"]
hydrate = ["leptos_core/hydrate", "leptos_router/hydrate", "leptos_macro/hydrate", "leptos_reactive/hydrate", "leptos_router/hydrate", "leptos_ssr/hydrate"]
ssr = ["leptos_core/ssr", "leptos_router/ssr", "leptos_macro/ssr", "leptos_reactive/ssr", "leptos_router/ssr", "leptos_ssr/ssr"]

View file

@ -104,6 +104,7 @@ where
cx.untrack(f)
}
#[cfg(not(feature = "csr"))]
pub fn create_component<F, T>(cx: Scope, f: F) -> T
where
F: FnOnce() -> T,

View file

@ -348,7 +348,7 @@ fn clean_children(
marker: &Marker,
replacement: Option<web_sys::Node>,
) -> Child {
//log::debug!("clean_children on {} with current = {current:?} and marker = {marker:#?} and replacement = {replacement:#?}", parent.node_name());
log::debug!("clean_children on {} with current = {current:?} and marker = {marker:#?} and replacement = {replacement:#?}", parent.node_name());
if marker == &Marker::NoChildren {
parent.set_text_content(Some(""));
@ -366,26 +366,31 @@ fn clean_children(
} else {
let mut inserted = false;
log::debug!("iterating over current nodes");
log::debug!("node = {} => {:?}", node.node_name(), node.node_value());
//log::debug!("node = {} => {:?}", node.node_name(), node.node_value());
for (idx, el) in nodes.iter().enumerate().rev() {
log::debug!("{idx}: {} => {:?}", el.node_name(), el.node_value());
//log::debug!("{idx}: {} => {:?}", el.node_name(), el.node_value());
if &node != el {
let is_parent =
el.parent_node() == Some(parent.clone().unchecked_into());
log::debug!("is_parent = {is_parent}");
//log::debug!("is_parent = {is_parent}");
if !inserted && idx == 0 {
log::debug!("!insert && idx == 0");
//log::debug!("!insert && idx == 0");
if is_parent {
log::debug!(
/* log::debug!(
"replacing child {}/{:?} with {}/{:?}",
el.node_name(),
el.node_value(),
node.node_name(),
node.node_value()
);
); */
replace_child(parent, &node, el);
} else {
log::debug!("inserting before");
/* log::debug!(
"inserting {:?} before {:?} on {:?}",
node.node_name(),
marker.as_some_node().map(|n| n.node_name()),
parent.outer_html()
); */
node = insert_before(parent, &node, marker.as_some_node());
}
} else {

View file

@ -195,21 +195,21 @@ fn element_to_tokens(
quote_spanned! {
span => //let #this_el_ident = #debug_name;
let #this_el_ident = #parent.clone().unchecked_into::<web_sys::Node>();
log::debug!("=> got {}", #this_el_ident.node_name());
//log::debug!("=> got {}\n\n{:?}", #this_el_ident.node_name(), #this_el_ident.dyn_ref::<web_sys::Element>().map(|el| el.outer_html()));
}
} else if let Some(prev_sib) = &prev_sib {
quote_spanned! {
span => //let #this_el_ident = #debug_name;
log::debug!("next_sibling ({})", #debug_name);
//log::debug!("next_sibling ({})", #debug_name);
let #this_el_ident = #prev_sib.next_sibling().unwrap_throw();
log::debug!("=> got {}", #this_el_ident.node_name());
//log::debug!("=> got {}", #this_el_ident.node_name());
}
} else {
quote_spanned! {
span => //let #this_el_ident = #debug_name;
log::debug!("first_child ({})", #debug_name);
//log::debug!("first_child ({})", #debug_name);
let #this_el_ident = #parent.first_child().unwrap_throw();
log::debug!("=> got {}", #this_el_ident.node_name());
//log::debug!("=> got {}", #this_el_ident.node_name());
}
};
navigations.push(this_nav);
@ -525,15 +525,15 @@ fn child_to_tokens(
let name = child_ident(*next_el_id, node);
let location = if let Some(sibling) = prev_sib {
quote_spanned! {
span => log::debug!("next sibling");
span => //log::debug!("-> next sibling");
let #name = #sibling.next_sibling().unwrap_throw();
log::debug!("=> next sibling = {}", #name.node_name());
//log::debug!("\tnext sibling = {}", #name.node_name());
}
} else {
quote_spanned! {
span => log::debug!("first child");
span => //log::debug!("\\|/ first child");
let #name = #parent.first_child().unwrap_throw();
log::debug!("=> next sibling = {}", #name.node_name());
//log::debug!("\tfirst child = {}", #name.node_name());
}
};
@ -674,14 +674,30 @@ fn create_component(node: &Node, mode: Mode) -> TokenStream {
let span = node.name_span().unwrap();
let component_props_name = Ident::new(&format!("{component_name}Props"), span);
let children = if node.children.is_empty() {
quote! {}
let (initialize_children, children) = if node.children.is_empty() {
(quote! {}, quote! {})
} else if node.children.len() == 1 {
let child = render_view(&node.children, mode);
quote! { .children(vec![#child]) }
if mode == Mode::Hydrate {
(
quote! {let children = vec![#child]; },
quote! { .children(children) },
)
} else {
(quote! {}, quote! { .children(vec![#child]) })
}
} else {
let children = render_view(&node.children, mode);
quote! { .children(#children) }
if mode == Mode::Hydrate {
(
quote! { let children = #children; },
quote! { .children(children) },
)
} else {
(quote! {}, quote! { .children(#children) })
}
};
let props = node.attributes.iter().filter_map(|attr| {
@ -744,6 +760,7 @@ fn create_component(node: &Node, mode: Mode) -> TokenStream {
if other_attrs.peek().is_none() {
quote_spanned! {
span => create_component(cx, move || {
#initialize_children
#component_name(
cx,
#component_props_name::builder()
@ -756,6 +773,7 @@ fn create_component(node: &Node, mode: Mode) -> TokenStream {
} else {
quote_spanned! {
span => create_component(cx, move || {
#initialize_children
let #component_name = #component_name(
cx,
#component_props_name::builder()

View file

@ -121,7 +121,7 @@ impl Scope {
#[cfg(any(feature = "csr", feature = "hydrate"))]
pub fn get_next_element(&self, template: &web_sys::Element) -> web_sys::Element {
log::debug!("get_next_element");
//log::debug!("get_next_element");
use wasm_bindgen::{JsCast, UnwrapThrowExt};
let cloned_template = |t: &web_sys::Element| {
@ -142,7 +142,6 @@ impl Scope {
let node = shared_context.registry.remove(&key.to_string());
if let Some(node) = node {
log::debug!("(get_next_element) hk {} => {}", key, node.node_name());
shared_context.completed.push(node.clone());
node
} else {

12
ssr/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "leptos_ssr"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos_reactive = { path = "../leptos_reactive" }
[features]
csr = ["leptos_reactive/csr"]
hydrate = ["leptos_reactive/hydrate"]
ssr = ["leptos_reactive/ssr"]

101
ssr/src/lib.rs Normal file
View file

@ -0,0 +1,101 @@
use leptos_reactive::Scope;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SsrNode {
Text(String),
Element(SsrElement),
DynamicText(String),
DynamicElement(SsrElement),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SsrElement {
is_root_el: bool,
tag_name: &'static str,
attrs: Vec<(&'static str, String)>,
inner_html: Option<String>,
children: Vec<SsrNode>,
}
impl SsrElement {
pub fn render_to_string(&self, cx: Scope) -> String {
let mut buf = String::new();
self.render_to_string_with_buf(cx, &mut buf);
buf
}
pub fn render_to_string_with_buf(&self, cx: Scope, buf: &mut String) {
// open tag
buf.push('<');
buf.push_str(&self.tag_name);
// hydration key
if self.is_root_el {
buf.push_str(" data-hk=\"");
buf.push_str(&cx.next_hydration_key().to_string());
buf.push('"');
}
// attributes
for (name, value) in &self.attrs {
if value.is_empty() {
buf.push(' ');
buf.push_str(name);
} else {
buf.push(' ');
buf.push_str(name);
buf.push_str("=\"");
buf.push_str(&value);
buf.push('"');
}
}
// children
if is_self_closing(&self.tag_name) {
buf.push_str("/>");
} else {
buf.push('>');
if let Some(inner_html) = &self.inner_html {
buf.push_str(inner_html);
} else {
for child in &self.children {
match child {
SsrNode::Text(value) => buf.push_str(value),
SsrNode::Element(el) => el.render_to_string_with_buf(cx, buf),
SsrNode::DynamicText(value) => {
buf.push_str("<!--#-->");
buf.push_str(value);
buf.push_str("<!--/-->");
}
SsrNode::DynamicElement(el) => {
buf.push_str("<!--#-->");
el.render_to_string_with_buf(cx, buf);
buf.push_str("<!--/-->");
}
}
}
}
}
}
}
fn is_self_closing(tag_name: &str) -> bool {
matches!(
tag_name,
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
)
}