mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 09:54:41 -05:00
Beginning work on SSR/hydration
This commit is contained in:
9
TODO.md
9
TODO.md
@@ -8,6 +8,11 @@
|
||||
- [ ] Router example
|
||||
- [ ] Styling and formatting
|
||||
- [ ] Get transitions working
|
||||
- [ ] SSR
|
||||
- [x] SSR
|
||||
- [ ] Hydration
|
||||
- [ ] Streaming HTML from server
|
||||
- [ ] Streaming `Resource`s
|
||||
- [ ] Docs (and clippy warning to insist on docs)
|
||||
- [ ] Read through + understand...
|
||||
- [ ] `Props` macro
|
||||
@@ -18,10 +23,6 @@
|
||||
- [ ] Scheduling effects/run effects at end of render
|
||||
- [ ] `batch()`
|
||||
- [ ] Portals
|
||||
- [ ] SSR
|
||||
- [ ] Macro
|
||||
- [ ] Streaming HTML from server
|
||||
- [ ] Streaming `Resource`s
|
||||
- [ ] Loaders
|
||||
- [ ] Tutorials + website
|
||||
- [ ] Scoped CSS
|
||||
|
||||
18
examples/counter-hydrate/Cargo.toml
Normal file
18
examples/counter-hydrate/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "counter-hydrate"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["hydrate"] }
|
||||
wee_alloc = "0.4"
|
||||
console_log = "0.2"
|
||||
log = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
10
examples/counter-hydrate/index.html
Normal file
10
examples/counter-hydrate/index.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="hydrated" data-hk="0"><button>-1</button><span>Value: <!--#-->0<!--/-->!</span><button>+1</button></div>
|
||||
<div id="mounted"></div>
|
||||
</body>
|
||||
</html>
|
||||
14
examples/counter-hydrate/src/lib.rs
Normal file
14
examples/counter-hydrate/src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use leptos::*;
|
||||
|
||||
pub fn simple_counter(cx: Scope) -> web_sys::Element {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
log::debug!("ok");
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=move |_| set_value(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<button on:click=move |_| set_value(|value| *value += 1)>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
23
examples/counter-hydrate/src/main.rs
Normal file
23
examples/counter-hydrate/src/main.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use counter_hydrate::simple_counter;
|
||||
use leptos::*;
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
pub fn main() {
|
||||
console_log::init_with_level(log::Level::Debug);
|
||||
hydrate(
|
||||
document()
|
||||
.get_element_by_id("hydrated")
|
||||
.unwrap()
|
||||
.unchecked_into(),
|
||||
simple_counter,
|
||||
);
|
||||
mount(
|
||||
document()
|
||||
.get_element_by_id("mounted")
|
||||
.unwrap()
|
||||
.unchecked_into(),
|
||||
simple_counter,
|
||||
);
|
||||
}
|
||||
@@ -11,5 +11,8 @@ leptos_reactive = { path = "../leptos_reactive" }
|
||||
leptos_router = { path = "../router" }
|
||||
|
||||
[features]
|
||||
default = ["browser"]
|
||||
browser = ["leptos_router/browser", "leptos_reactive/browser"]
|
||||
default = ["browser", "csr"]
|
||||
browser = ["leptos_router/browser", "leptos_reactive/browser"]
|
||||
csr = ["leptos_macro/client"]
|
||||
ssr = ["leptos_macro/ssr", "leptos_dom/server", "leptos_router/server"]
|
||||
hydrate = ["leptos_macro/hydrate"]
|
||||
@@ -51,4 +51,9 @@ features = [
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
opt-level = 'z'
|
||||
|
||||
[features]
|
||||
default = ["browser"]
|
||||
browser = []
|
||||
server = []
|
||||
@@ -10,6 +10,32 @@ pub enum Attribute {
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
impl Attribute {
|
||||
pub fn as_value_string(&self, attr_name: &'static str) -> String {
|
||||
match self {
|
||||
Attribute::String(value) => format!("{attr_name}=\"{value}\""),
|
||||
Attribute::Fn(f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(f) = value {
|
||||
value = f();
|
||||
}
|
||||
value.as_value_string(attr_name)
|
||||
}
|
||||
Attribute::Option(value) => value
|
||||
.as_ref()
|
||||
.map(|value| format!("{attr_name}=\"{value}\""))
|
||||
.unwrap_or_default(),
|
||||
Attribute::Bool(include) => {
|
||||
if *include {
|
||||
attr_name.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Attribute {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::rc::Rc;
|
||||
use leptos_reactive::Scope;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
type Node = web_sys::Node;
|
||||
use crate::Node;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Child {
|
||||
@@ -14,6 +14,25 @@ pub enum Child {
|
||||
Nodes(Vec<Node>),
|
||||
}
|
||||
|
||||
impl Child {
|
||||
#[cfg(feature = "server")]
|
||||
pub fn as_child_string(&self) -> String {
|
||||
match self {
|
||||
Child::Null => String::new(),
|
||||
Child::Text(text) => text.to_string(),
|
||||
Child::Fn(f) => {
|
||||
let mut value = f();
|
||||
while let Child::Fn(f) = value {
|
||||
value = f();
|
||||
}
|
||||
value.as_child_string()
|
||||
}
|
||||
Child::Node(node) => node.to_string(),
|
||||
Child::Nodes(nodes) => nodes.iter().cloned().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Child {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
@@ -54,18 +73,21 @@ impl IntoChild for String {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
impl IntoChild for web_sys::Node {
|
||||
fn into_child(self, _cx: Scope) -> Child {
|
||||
Child::Node(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
impl IntoChild for web_sys::Text {
|
||||
fn into_child(self, _cx: Scope) -> Child {
|
||||
Child::Node(self.unchecked_into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
impl IntoChild for web_sys::Element {
|
||||
fn into_child(self, _cx: Scope) -> Child {
|
||||
Child::Node(self.unchecked_into())
|
||||
@@ -84,12 +106,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
impl IntoChild for Vec<web_sys::Node> {
|
||||
fn into_child(self, _cx: Scope) -> Child {
|
||||
Child::Nodes(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
impl IntoChild for Vec<web_sys::Element> {
|
||||
fn into_child(self, _cx: Scope) -> Child {
|
||||
Child::Nodes(
|
||||
|
||||
@@ -1,40 +1,57 @@
|
||||
mod attribute;
|
||||
mod child;
|
||||
mod class;
|
||||
#[cfg(not(feature = "server"))]
|
||||
mod event_delegation;
|
||||
pub mod logging;
|
||||
#[cfg(not(feature = "server"))]
|
||||
mod operations;
|
||||
mod property;
|
||||
#[cfg(not(feature = "server"))]
|
||||
mod reconcile;
|
||||
#[cfg(not(feature = "server"))]
|
||||
mod render;
|
||||
|
||||
pub use attribute::*;
|
||||
pub use child::*;
|
||||
pub use class::*;
|
||||
pub use logging::*;
|
||||
#[cfg(not(feature = "server"))]
|
||||
pub use operations::*;
|
||||
pub use property::*;
|
||||
#[cfg(not(feature = "server"))]
|
||||
pub use render::*;
|
||||
|
||||
pub use js_sys;
|
||||
pub use wasm_bindgen;
|
||||
pub use web_sys;
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
pub type Element = web_sys::Element;
|
||||
#[cfg(feature = "server")]
|
||||
pub type Element = String;
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
pub type Node = web_sys::Node;
|
||||
#[cfg(feature = "server")]
|
||||
pub type Node = String;
|
||||
|
||||
use leptos_reactive::{create_scope, Scope};
|
||||
pub use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
pub trait Mountable {
|
||||
fn mount(&self, parent: &web_sys::Element);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
impl Mountable for Element {
|
||||
fn mount(&self, parent: &web_sys::Element) {
|
||||
parent.append_child(self).unwrap_throw();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
impl Mountable for Vec<Element> {
|
||||
fn mount(&self, parent: &web_sys::Element) {
|
||||
for element in self {
|
||||
@@ -43,6 +60,7 @@ impl Mountable for Vec<Element> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
pub fn mount_to_body<T, F>(f: F)
|
||||
where
|
||||
F: Fn(Scope) -> T + 'static,
|
||||
@@ -51,6 +69,7 @@ where
|
||||
mount(document().body().unwrap_throw(), f)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
pub fn mount<T, F>(parent: web_sys::HtmlElement, f: F)
|
||||
where
|
||||
F: Fn(Scope) -> T + 'static,
|
||||
@@ -63,6 +82,21 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
pub fn hydrate<T, F>(parent: web_sys::HtmlElement, f: F)
|
||||
where
|
||||
F: Fn(Scope) -> T + 'static,
|
||||
T: Mountable,
|
||||
{
|
||||
// running "hydrate" intentionally leaks the memory,
|
||||
// as the "hydrate" has no parent that can clean it up
|
||||
let _ = create_scope(move |cx| {
|
||||
cx.begin_hydration();
|
||||
(f(cx));
|
||||
cx.complete_hydration();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn create_component<F, T>(cx: Scope, f: F) -> T
|
||||
where
|
||||
F: Fn() -> T,
|
||||
@@ -74,7 +108,7 @@ where
|
||||
#[macro_export]
|
||||
macro_rules! is_server {
|
||||
() => {
|
||||
!cfg!(target_arch = "wasm32")
|
||||
cfg!(feature = "server")
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,3 +13,14 @@ quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
|
||||
syn-rsx = "0.8.1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos_core = { path = "../leptos_core" }
|
||||
leptos_dom = { path = "../leptos_dom", features = ["server"] }
|
||||
leptos_reactive = { path = "../leptos_reactive" }
|
||||
|
||||
[features]
|
||||
default = ["client"]
|
||||
client = []
|
||||
hydrate = []
|
||||
ssr = []
|
||||
@@ -3,43 +3,34 @@ use quote::ToTokens;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
use syn_rsx::{parse, Node, NodeType};
|
||||
|
||||
enum Mode {
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Mode {
|
||||
Client,
|
||||
Hydrate,
|
||||
Dehydrate,
|
||||
Static,
|
||||
Ssr,
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Self::Client
|
||||
if cfg!(feature = "ssr") {
|
||||
Mode::Ssr
|
||||
} else if cfg!(feature = "hydrate") {
|
||||
Mode::Hydrate
|
||||
} else {
|
||||
Mode::Client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod csr;
|
||||
use csr::client_side_rendering;
|
||||
mod view;
|
||||
use view::render_view;
|
||||
mod component;
|
||||
mod props;
|
||||
|
||||
#[proc_macro]
|
||||
pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
match parse(tokens) {
|
||||
Ok(nodes) => {
|
||||
let mode = std::env::var("LEPTOS_MODE")
|
||||
.map(|mode| match mode.to_lowercase().as_str() {
|
||||
"client" => Mode::Client,
|
||||
"hydrate" => Mode::Hydrate,
|
||||
"dehydrate" => Mode::Dehydrate,
|
||||
"static" => Mode::Static,
|
||||
_ => Mode::Client,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
match mode {
|
||||
Mode::Client => client_side_rendering(&nodes),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
Ok(nodes) => render_view(&nodes, Mode::default()),
|
||||
Err(error) => error.to_compile_error(),
|
||||
}
|
||||
.into()
|
||||
|
||||
@@ -4,20 +4,20 @@ use syn::{spanned::Spanned, ExprPath};
|
||||
use syn_rsx::{Node, NodeName, NodeType};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::is_component_node;
|
||||
use crate::{is_component_node, Mode};
|
||||
|
||||
pub fn client_side_rendering(nodes: &[Node]) -> TokenStream {
|
||||
pub(crate) fn render_view(nodes: &[Node], mode: Mode) -> TokenStream {
|
||||
let template_uid = Ident::new(
|
||||
&format!("TEMPLATE_{}", Uuid::new_v4().simple()),
|
||||
Span::call_site(),
|
||||
);
|
||||
|
||||
if nodes.len() == 1 {
|
||||
first_node_to_tokens(&template_uid, &nodes[0])
|
||||
first_node_to_tokens(&template_uid, &nodes[0], mode)
|
||||
} else {
|
||||
let nodes = nodes
|
||||
.iter()
|
||||
.map(|node| first_node_to_tokens(&template_uid, node));
|
||||
.map(|node| first_node_to_tokens(&template_uid, node, mode));
|
||||
quote! {
|
||||
{
|
||||
vec![
|
||||
@@ -28,14 +28,14 @@ pub fn client_side_rendering(nodes: &[Node]) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
fn first_node_to_tokens(template_uid: &Ident, node: &Node) -> TokenStream {
|
||||
fn first_node_to_tokens(template_uid: &Ident, node: &Node, mode: Mode) -> TokenStream {
|
||||
match node.node_type {
|
||||
NodeType::Doctype | NodeType::Comment => quote! {},
|
||||
NodeType::Fragment => {
|
||||
let nodes = node
|
||||
.children
|
||||
.iter()
|
||||
.map(|node| first_node_to_tokens(template_uid, node));
|
||||
.map(|node| first_node_to_tokens(template_uid, node, mode));
|
||||
quote! {
|
||||
{
|
||||
vec![
|
||||
@@ -44,7 +44,7 @@ fn first_node_to_tokens(template_uid: &Ident, node: &Node) -> TokenStream {
|
||||
}
|
||||
}
|
||||
}
|
||||
NodeType::Element => root_element_to_tokens(template_uid, node),
|
||||
NodeType::Element => root_element_to_tokens(template_uid, node, mode),
|
||||
NodeType::Block => node
|
||||
.value
|
||||
.as_ref()
|
||||
@@ -60,37 +60,63 @@ fn first_node_to_tokens(template_uid: &Ident, node: &Node) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
fn root_element_to_tokens(template_uid: &Ident, node: &Node) -> TokenStream {
|
||||
fn root_element_to_tokens(template_uid: &Ident, node: &Node, mode: Mode) -> TokenStream {
|
||||
let mut template = String::new();
|
||||
let mut navigations = Vec::new();
|
||||
let mut expressions = Vec::new();
|
||||
|
||||
if is_component_node(node) {
|
||||
create_component(node)
|
||||
create_component(node, mode)
|
||||
} else {
|
||||
element_to_tokens(
|
||||
node,
|
||||
&Ident::new("root", Span::call_site()),
|
||||
None,
|
||||
&mut 0,
|
||||
&mut 0,
|
||||
&mut template,
|
||||
&mut navigations,
|
||||
&mut expressions,
|
||||
true,
|
||||
mode,
|
||||
);
|
||||
|
||||
quote! {
|
||||
{
|
||||
thread_local! {
|
||||
static #template_uid: web_sys::HtmlTemplateElement = leptos_dom::create_template(#template);
|
||||
match mode {
|
||||
Mode::Ssr => {
|
||||
quote! {
|
||||
format!(
|
||||
#template,
|
||||
#(#expressions),*
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// create the root element from which navigations and expressions will begin
|
||||
let generate_root = match mode {
|
||||
// SSR is just going to return a format string, so no root/navigations
|
||||
Mode::Ssr => unreachable!(),
|
||||
// for CSR, just clone the template and take the first child as the root
|
||||
Mode::Client => quote! {
|
||||
let root = #template_uid.with(|template| leptos_dom::clone_template(template)).first_element_child().unwrap_throw();
|
||||
},
|
||||
// for hydration, use get_next_element(), which will either draw from an SSRed node or clone the template
|
||||
Mode::Hydrate => quote! {
|
||||
let root = #template_uid.with(|template| cx.get_next_element(template));
|
||||
},
|
||||
};
|
||||
let root = #template_uid.with(|template| leptos_dom::clone_template(template));
|
||||
//let root = leptos_dom::clone_template(&leptos_dom::create_template(#template));
|
||||
|
||||
#(#navigations);*
|
||||
#(#expressions);*;
|
||||
quote! {
|
||||
thread_local! {
|
||||
static #template_uid: web_sys::HtmlTemplateElement = leptos_dom::create_template(#template);
|
||||
};
|
||||
|
||||
// returns the first child created in the template
|
||||
root.first_element_child().unwrap_throw()
|
||||
#generate_root
|
||||
|
||||
#(#navigations);*
|
||||
#(#expressions);*;
|
||||
|
||||
root
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,28 +135,44 @@ fn element_to_tokens(
|
||||
parent: &Ident,
|
||||
prev_sib: Option<Ident>,
|
||||
next_el_id: &mut usize,
|
||||
next_co_id: &mut usize,
|
||||
template: &mut String,
|
||||
navigations: &mut Vec<TokenStream>,
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
is_root_el: bool,
|
||||
mode: Mode,
|
||||
) -> Ident {
|
||||
// create this element
|
||||
*next_el_id += 1;
|
||||
let this_el_ident = child_ident(*next_el_id, node);
|
||||
|
||||
// TEMPLATE: open tag
|
||||
// TEMPLATE: open tag — true for any mode
|
||||
let name_str = node.name_as_string().unwrap();
|
||||
template.push('<');
|
||||
template.push_str(&name_str);
|
||||
|
||||
// for SSR: add a hydration key
|
||||
if mode == Mode::Ssr && is_root_el {
|
||||
template.push_str(" data-hk=\"{}\"");
|
||||
expressions.push(quote! {
|
||||
cx.next_hydration_key()
|
||||
});
|
||||
}
|
||||
|
||||
// attributes
|
||||
for attr in &node.attributes {
|
||||
attr_to_tokens(attr, &this_el_ident, template, expressions);
|
||||
attr_to_tokens(attr, &this_el_ident, template, expressions, mode);
|
||||
}
|
||||
|
||||
// navigation for this el
|
||||
let debug_name = debug_name(node);
|
||||
let span = span(node);
|
||||
let this_nav = if let Some(prev_sib) = &prev_sib {
|
||||
let this_nav = if is_root_el {
|
||||
quote_spanned! {
|
||||
span => let #this_el_ident = #debug_name;
|
||||
let #this_el_ident = #parent.clone().unchecked_into::<web_sys::Node>();
|
||||
}
|
||||
} else if let Some(prev_sib) = &prev_sib {
|
||||
quote_spanned! {
|
||||
span => let #this_el_ident = #debug_name;
|
||||
let #this_el_ident = #prev_sib.next_sibling().unwrap_throw();
|
||||
@@ -138,7 +180,7 @@ fn element_to_tokens(
|
||||
} else {
|
||||
quote_spanned! {
|
||||
span => let #this_el_ident = #debug_name;
|
||||
let #this_el_ident = "first child wing C"; let #this_el_ident = #parent.first_child().unwrap_throw();
|
||||
let #this_el_ident = #parent.first_child().unwrap_throw();
|
||||
}
|
||||
};
|
||||
navigations.push(this_nav);
|
||||
@@ -187,10 +229,12 @@ fn element_to_tokens(
|
||||
if idx == 0 { None } else { prev_sib.clone() },
|
||||
next_sib,
|
||||
next_el_id,
|
||||
next_co_id,
|
||||
template,
|
||||
navigations,
|
||||
expressions,
|
||||
multi,
|
||||
mode,
|
||||
);
|
||||
|
||||
prev_sib = match curr_id {
|
||||
@@ -200,7 +244,7 @@ fn element_to_tokens(
|
||||
};
|
||||
}
|
||||
|
||||
// TEMPLATE: close tag
|
||||
// TEMPLATE: close tag (same for any mode)
|
||||
template.push_str("</");
|
||||
template.push_str(&name_str);
|
||||
template.push('>');
|
||||
@@ -213,6 +257,7 @@ fn attr_to_tokens(
|
||||
el_id: &Ident,
|
||||
template: &mut String,
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
mode: Mode,
|
||||
) {
|
||||
let name = node
|
||||
.name_as_string()
|
||||
@@ -234,48 +279,60 @@ fn attr_to_tokens(
|
||||
|
||||
// refs
|
||||
if name == "ref" {
|
||||
expressions.push(match &node.value {
|
||||
Some(expr) => {
|
||||
if let Some(ident) = expr_to_ident(expr) {
|
||||
quote_spanned! {
|
||||
span =>
|
||||
// we can't pass by reference because the _el won't live long enough (it's dropped when template returns)
|
||||
// so we clone here; this will be unnecessary if it's the last attribute, but very necessary otherwise
|
||||
#ident = #el_id.clone().unchecked_into::<web_sys::Element>();
|
||||
// refs mean nothing in SSR
|
||||
if mode != Mode::Ssr {
|
||||
expressions.push(match &node.value {
|
||||
Some(expr) => {
|
||||
if let Some(ident) = expr_to_ident(expr) {
|
||||
quote_spanned! {
|
||||
span =>
|
||||
// we can't pass by reference because the _el won't live long enough (it's dropped when template returns)
|
||||
// so we clone here; this will be unnecessary if it's the last attribute, but very necessary otherwise
|
||||
#ident = #el_id.clone().unchecked_into::<web_sys::Element>();
|
||||
}
|
||||
} else {
|
||||
panic!("'ref' needs to be passed a variable name")
|
||||
}
|
||||
} else {
|
||||
panic!("'ref' needs to be passed a variable name")
|
||||
}
|
||||
}
|
||||
_ => panic!("'ref' needs to be passed a variable name"),
|
||||
})
|
||||
_ => panic!("'ref' needs to be passed a variable name"),
|
||||
})
|
||||
}
|
||||
}
|
||||
// Event Handlers
|
||||
else if name.starts_with("on:") {
|
||||
let event_name = name.replacen("on:", "", 1);
|
||||
let handler = node
|
||||
.value
|
||||
.as_ref()
|
||||
.expect("event listener attributes need a value");
|
||||
expressions.push(quote_spanned! {
|
||||
span => add_event_listener(#el_id.unchecked_ref(), #event_name, #handler);
|
||||
});
|
||||
if mode != Mode::Ssr {
|
||||
let event_name = name.replacen("on:", "", 1);
|
||||
let handler = node
|
||||
.value
|
||||
.as_ref()
|
||||
.expect("event listener attributes need a value");
|
||||
expressions.push(quote_spanned! {
|
||||
span => add_event_listener(#el_id.unchecked_ref(), #event_name, #handler);
|
||||
});
|
||||
}
|
||||
}
|
||||
// Properties
|
||||
else if name.starts_with("prop:") {
|
||||
let name = name.replacen("prop:", "", 1);
|
||||
let value = node.value.as_ref().expect("prop: blocks need values");
|
||||
expressions.push(quote_spanned! {
|
||||
// can't set properties in SSR
|
||||
if mode != Mode::Ssr {
|
||||
let name = name.replacen("prop:", "", 1);
|
||||
let value = node.value.as_ref().expect("prop: blocks need values");
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos_dom::property(cx, #el_id.unchecked_ref(), #name, #value.into_property(cx))
|
||||
});
|
||||
}
|
||||
}
|
||||
// Classes
|
||||
else if name.starts_with("class:") {
|
||||
let name = name.replacen("class:", "", 1);
|
||||
let value = node.value.as_ref().expect("class: attributes need values");
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos_dom::class(cx, #el_id.unchecked_ref(), #name, #value.into_class(cx))
|
||||
});
|
||||
if mode == Mode::Ssr {
|
||||
todo!()
|
||||
} else {
|
||||
let name = name.replacen("class:", "", 1);
|
||||
let value = node.value.as_ref().expect("class: attributes need values");
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos_dom::class(cx, #el_id.unchecked_ref(), #name, #value.into_class(cx))
|
||||
});
|
||||
}
|
||||
}
|
||||
// Attributes
|
||||
else {
|
||||
@@ -297,12 +354,20 @@ fn attr_to_tokens(
|
||||
template.push('"');
|
||||
}
|
||||
|
||||
// For client-side rendering, dynamic attributes don't need to be rendered in the template
|
||||
// They'll immediately be set synchronously before the cloned template is mounted
|
||||
// Dynamic attributes are handled differently depending on the rendering mode
|
||||
AttributeValue::Dynamic(value) => {
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos_dom::attribute(cx, #el_id.unchecked_ref(), #name, #value.into_attribute(cx))
|
||||
});
|
||||
if mode == Mode::Ssr {
|
||||
template.push_str(" {}");
|
||||
expressions.push(quote_spanned! {
|
||||
span => #value.into_attribute(cx).as_value_string(#name)
|
||||
});
|
||||
} else {
|
||||
// For client-side rendering, dynamic attributes don't need to be rendered in the template
|
||||
// They'll immediately be set synchronously before the cloned template is mounted
|
||||
expressions.push(quote_spanned! {
|
||||
span => leptos_dom::attribute(cx, #el_id.unchecked_ref(), #name, #value.into_attribute(cx))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,24 +386,37 @@ fn child_to_tokens(
|
||||
prev_sib: Option<Ident>,
|
||||
next_sib: Option<Ident>,
|
||||
next_el_id: &mut usize,
|
||||
next_co_id: &mut usize,
|
||||
template: &mut String,
|
||||
navigations: &mut Vec<TokenStream>,
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
multi: bool,
|
||||
mode: Mode,
|
||||
) -> PrevSibChange {
|
||||
match node.node_type {
|
||||
NodeType::Element => {
|
||||
if is_component_node(node) {
|
||||
component_to_tokens(node, Some(parent), next_sib, expressions, multi)
|
||||
component_to_tokens(
|
||||
node,
|
||||
Some(parent),
|
||||
next_sib,
|
||||
template,
|
||||
expressions,
|
||||
multi,
|
||||
mode,
|
||||
)
|
||||
} else {
|
||||
PrevSibChange::Sib(element_to_tokens(
|
||||
node,
|
||||
parent,
|
||||
prev_sib,
|
||||
next_el_id,
|
||||
next_co_id,
|
||||
template,
|
||||
navigations,
|
||||
expressions,
|
||||
false,
|
||||
mode,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -370,8 +448,7 @@ fn child_to_tokens(
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {
|
||||
span => let #name = "first child wing A";
|
||||
let #name = #parent.first_child().unwrap_throw();
|
||||
span => let #name = #parent.first_child().unwrap_throw();
|
||||
}
|
||||
};
|
||||
navigations.push(location);
|
||||
@@ -382,17 +459,43 @@ fn child_to_tokens(
|
||||
*next_el_id += 1;
|
||||
let name = child_ident(*next_el_id, node);
|
||||
|
||||
template.push_str("<!>");
|
||||
let location = if let Some(sibling) = &prev_sib {
|
||||
quote_spanned! {
|
||||
span => let #name = #sibling.next_sibling().unwrap_throw();
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {
|
||||
span => let #name = "first child wing B"; let #name = #parent.first_child().unwrap_throw();
|
||||
span => let #name = #parent.first_child().unwrap_throw();
|
||||
}
|
||||
};
|
||||
navigations.push(location);
|
||||
|
||||
// these markers are one of the primary templating differences across modes
|
||||
match mode {
|
||||
// in CSR, simply insert a comment node: it will be picked up and replaced with the value
|
||||
Mode::Client => {
|
||||
template.push_str("<!>");
|
||||
navigations.push(location);
|
||||
}
|
||||
// when hydrating, a text node will be generated by SSR; in the hydration/CSR template,
|
||||
// wrap it with comments that mark where it begins and ends
|
||||
Mode::Hydrate => {
|
||||
*next_el_id += 1;
|
||||
let el = child_ident(*next_el_id, node);
|
||||
*next_co_id += 1;
|
||||
let co = comment_ident(*next_co_id, node);
|
||||
|
||||
template.push_str("<!#><!/>");
|
||||
navigations.push(quote! {
|
||||
#location;
|
||||
let (#el, #co) = cx.get_next_marker(#name);
|
||||
});
|
||||
}
|
||||
// in SSR, it needs to both wrap with comments and insert a hole for an existing value
|
||||
Mode::Ssr => {
|
||||
template.push_str("<!--#-->{}<!--/-->");
|
||||
// TODO push expression here, or elsewhere?
|
||||
}
|
||||
}
|
||||
|
||||
let before = match &next_sib {
|
||||
Some(child) => quote! { leptos::Marker::BeforeChild(#child.clone()) },
|
||||
@@ -407,15 +510,21 @@ fn child_to_tokens(
|
||||
|
||||
let value = node.value_as_block().expect("no block value");
|
||||
|
||||
expressions.push(quote! {
|
||||
leptos::insert(
|
||||
cx,
|
||||
#parent.clone(),
|
||||
#value.into_child(cx),
|
||||
#before,
|
||||
None,
|
||||
);
|
||||
});
|
||||
if mode == Mode::Ssr {
|
||||
expressions.push(quote! {
|
||||
#value.into_child(cx).as_child_string()
|
||||
});
|
||||
} else {
|
||||
expressions.push(quote! {
|
||||
leptos::insert(
|
||||
cx,
|
||||
#parent.clone(),
|
||||
#value.into_child(cx),
|
||||
#before,
|
||||
None,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
PrevSibChange::Sib(name)
|
||||
} else {
|
||||
@@ -433,15 +542,21 @@ fn child_to_tokens(
|
||||
|
||||
let value = node.value_as_block().expect("no block value");
|
||||
|
||||
expressions.push(quote! {
|
||||
leptos::insert(
|
||||
cx,
|
||||
#parent.clone(),
|
||||
#value.into_child(cx),
|
||||
#before,
|
||||
None,
|
||||
);
|
||||
});
|
||||
if mode == Mode::Ssr {
|
||||
expressions.push(quote! {
|
||||
#value.into_child(cx).as_child_string()
|
||||
});
|
||||
} else {
|
||||
expressions.push(quote! {
|
||||
leptos::insert(
|
||||
cx,
|
||||
#parent.clone(),
|
||||
#value.into_child(cx),
|
||||
#before,
|
||||
None,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
PrevSibChange::Skip
|
||||
}
|
||||
@@ -455,10 +570,12 @@ fn component_to_tokens(
|
||||
node: &Node,
|
||||
parent: Option<&Ident>,
|
||||
next_sib: Option<Ident>,
|
||||
template: &mut String,
|
||||
expressions: &mut Vec<TokenStream>,
|
||||
multi: bool,
|
||||
mode: Mode,
|
||||
) -> PrevSibChange {
|
||||
let create_component = create_component(node);
|
||||
let create_component = create_component(node, mode);
|
||||
|
||||
if let Some(parent) = parent {
|
||||
let before = match &next_sib {
|
||||
@@ -472,15 +589,22 @@ fn component_to_tokens(
|
||||
}
|
||||
};
|
||||
|
||||
expressions.push(quote! {
|
||||
leptos::insert(
|
||||
cx,
|
||||
#parent.clone(),
|
||||
#create_component.into_child(cx),
|
||||
#before,
|
||||
None,
|
||||
);
|
||||
});
|
||||
if mode == Mode::Ssr {
|
||||
template.push_str("<!--#-->{}<!--/-->");
|
||||
expressions.push(quote! {
|
||||
#create_component.into_child(cx).as_child_string()
|
||||
});
|
||||
} else {
|
||||
expressions.push(quote! {
|
||||
leptos::insert(
|
||||
cx,
|
||||
#parent.clone(),
|
||||
#create_component.into_child(cx),
|
||||
#before,
|
||||
None,
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
expressions.push(create_component)
|
||||
}
|
||||
@@ -488,7 +612,7 @@ fn component_to_tokens(
|
||||
PrevSibChange::Skip
|
||||
}
|
||||
|
||||
fn create_component(node: &Node) -> TokenStream {
|
||||
fn create_component(node: &Node, mode: Mode) -> TokenStream {
|
||||
let component_name = ident_from_tag_name(node.name.as_ref().unwrap());
|
||||
let span = node.name_span().unwrap();
|
||||
let component_props_name = Ident::new(&format!("{component_name}Props"), span);
|
||||
@@ -496,10 +620,10 @@ fn create_component(node: &Node) -> TokenStream {
|
||||
let children = if node.children.is_empty() {
|
||||
quote! {}
|
||||
} else if node.children.len() == 1 {
|
||||
let child = client_side_rendering(&node.children);
|
||||
let child = render_view(&node.children, mode);
|
||||
quote! { .children(vec![#child]) }
|
||||
} else {
|
||||
let children = client_side_rendering(&node.children);
|
||||
let children = render_view(&node.children, mode);
|
||||
quote! { .children(#children) }
|
||||
};
|
||||
|
||||
@@ -600,6 +724,15 @@ fn child_ident(el_id: usize, node: &Node) -> Ident {
|
||||
}
|
||||
}
|
||||
|
||||
fn comment_ident(co_id: usize, node: &Node) -> Ident {
|
||||
let id = format!("_co{co_id}");
|
||||
match node.node_type {
|
||||
NodeType::Element => Ident::new(&id, node.name_span().unwrap()),
|
||||
NodeType::Text | NodeType::Block => Ident::new(&id, node.value.as_ref().unwrap().span()),
|
||||
_ => panic!("invalid child node type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
|
||||
match tag_name {
|
||||
NodeName::Path(path) => path
|
||||
59
leptos_macro/tests/ssr.rs
Normal file
59
leptos_macro/tests/ssr.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn simple_ssr_test() {
|
||||
use leptos_dom::*;
|
||||
use leptos_macro::view;
|
||||
use leptos_reactive::{create_scope, create_signal};
|
||||
|
||||
_ = create_scope(|cx| {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let rendered = view! {
|
||||
<div>
|
||||
<button on:click=move |_| set_value(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<button on:click=move |_| set_value(|value| *value += 1)>"+1"</button>
|
||||
</div>
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
r#"<div data-hk="0"><button>-1</button><span>Value: <!--#-->0<!--/-->!</span><button>+1</button></div>"# //r#"<div data-hk="0" id="hydrated" data-hk="0"><button>-1</button><span>Value: <!--#-->0<!--/-->!</span><button>+1</button></div>"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn ssr_test_with_components() {
|
||||
use leptos_core as leptos;
|
||||
use leptos_core::Prop;
|
||||
use leptos_dom::*;
|
||||
use leptos_macro::*;
|
||||
use leptos_reactive::{create_scope, create_signal, Scope};
|
||||
|
||||
#[component]
|
||||
fn Counter(cx: Scope, initial_value: i32) -> Element {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=move |_| set_value(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<button on:click=move |_| set_value(|value| *value += 1)>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
_ = create_scope(|cx| {
|
||||
let rendered = view! {
|
||||
<div class="counters">
|
||||
<Counter initial_value=1/>
|
||||
<Counter initial_value=2/>
|
||||
</div>
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
r#"<div data-hk="0" class="counters"><!--#--><div data-hk="1"><button>-1</button><span>Value: <!--#-->0<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk="2"><button>-1</button><span>Value: <!--#-->0<!--/-->!</span><button>+1</button></div><!--/--></div>"#
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -3,12 +3,14 @@ use crate::{
|
||||
ScopeState, SignalId, SignalState, Subscriber, TransitionState,
|
||||
};
|
||||
use slotmap::SlotMap;
|
||||
use std::cell::RefCell;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::fmt::Debug;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct Runtime {
|
||||
pub(crate) is_hydrating: Cell<bool>,
|
||||
pub(crate) hydration_key: Cell<usize>,
|
||||
pub(crate) stack: RefCell<Vec<Subscriber>>,
|
||||
pub(crate) scopes: RefCell<SlotMap<ScopeId, Rc<ScopeState>>>,
|
||||
pub(crate) transition: RefCell<Option<Rc<TransitionState>>>,
|
||||
@@ -147,6 +149,24 @@ impl Runtime {
|
||||
self.stack.replace(prev_stack);
|
||||
untracked_result
|
||||
}
|
||||
|
||||
pub fn is_hydrating(&self) -> bool {
|
||||
self.is_hydrating.get()
|
||||
}
|
||||
|
||||
pub fn begin_hydration(&self) {
|
||||
self.is_hydrating.set(true);
|
||||
}
|
||||
|
||||
pub fn complete_hydration(&self) {
|
||||
self.is_hydrating.set(false);
|
||||
}
|
||||
|
||||
pub fn next_hydration_key(&self) -> usize {
|
||||
let next = self.hydration_key.get();
|
||||
self.hydration_key.set(next + 1);
|
||||
next
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Runtime {
|
||||
|
||||
@@ -83,6 +83,22 @@ impl Scope {
|
||||
})
|
||||
// removing from the runtime will drop this Scope, and all its Signals/Effects/Memos
|
||||
}
|
||||
|
||||
pub fn begin_hydration(&self) {
|
||||
self.runtime.begin_hydration();
|
||||
}
|
||||
|
||||
pub fn complete_hydration(&self) {
|
||||
self.runtime.complete_hydration();
|
||||
}
|
||||
|
||||
pub fn is_hydrating(&self) -> bool {
|
||||
self.runtime.is_hydrating()
|
||||
}
|
||||
|
||||
pub fn next_hydration_key(&self) -> usize {
|
||||
self.runtime.next_hydration_key()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScopeDisposer(pub(crate) Box<dyn FnOnce()>);
|
||||
|
||||
@@ -35,4 +35,5 @@ features = [
|
||||
|
||||
[features]
|
||||
default = ["dep:url", "dep:regex"]
|
||||
browser = ["dep:js-sys"]
|
||||
browser = ["dep:js-sys", "leptos_dom/browser"]
|
||||
server = ["leptos_dom/server"]
|
||||
|
||||
Reference in New Issue
Block a user