mirror of
https://github.com/leptos-rs/leptos
synced 2024-09-20 06:21:57 +00:00
Picking away at hydration bugs
This commit is contained in:
parent
293bece526
commit
4a8339df18
10 changed files with 201 additions and 56 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
})}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"]
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
12
ssr/Cargo.toml
Normal 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
101
ssr/src/lib.rs
Normal 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"
|
||||
)
|
||||
}
|
Loading…
Reference in a new issue