Implement classes in SSR macro path

This commit is contained in:
Greg Johnston 2022-12-15 07:56:31 -05:00
parent 3ef64bd372
commit 01013b00e5
9 changed files with 145 additions and 36 deletions

View File

@ -16,9 +16,13 @@ leptos_macro = { path = "../leptos_macro", default-features = false, version = "
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.18" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.18" }
[dev-dependencies]
leptos = { path = ".", default-features = false }
[features]
default = ["csr", "serde"]
csr = [
"leptos/csr",
"leptos_core/csr",
"leptos_dom/web",
"leptos_macro/csr",
@ -26,6 +30,7 @@ csr = [
"leptos_server/csr",
]
hydrate = [
"leptos/hydrate",
"leptos_core/hydrate",
"leptos_dom/web",
"leptos_macro/hydrate",
@ -33,6 +38,7 @@ hydrate = [
"leptos_server/hydrate",
]
ssr = [
"leptos/ssr",
"leptos_dom/ssr",
"leptos_core/ssr",
"leptos_macro/ssr",
@ -40,6 +46,7 @@ ssr = [
"leptos_server/ssr",
]
stable = [
"leptos/stable",
"leptos_core/stable",
"leptos_dom/stable",
"leptos_macro/stable",

View File

@ -87,7 +87,7 @@
//! use leptos::*;
//!
//! #[component]
//! pub fn SimpleCounter(cx: Scope, initial_value: i32) -> Element {
//! pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
//! // create a reactive signal with the initial value
//! let (value, set_value) = create_signal(cx, initial_value);
//!
@ -116,7 +116,7 @@
//! # if false { // can't run in doctests
//!
//! #[component]
//! fn SimpleCounter(cx: Scope, initial_value: i32) -> Element {
//! fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
//! todo!()
//! }
//!

View File

@ -1,9 +1,7 @@
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn simple_ssr_test() {
use leptos_dom::*;
use leptos_macro::view;
use leptos_reactive::{create_runtime, create_scope, create_signal};
use leptos::*;
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 0);
@ -11,13 +9,13 @@ fn simple_ssr_test() {
cx,
<div>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
<span>"Value: " {move || value.get().to_string()} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
</div>
};
assert_eq!(
rendered,
rendered.into_view(cx).render_to_string(cx),
r#"<div data-hk="0-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>"#
);
});
@ -26,20 +24,16 @@ fn simple_ssr_test() {
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[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_runtime, create_scope, create_signal, Scope};
use leptos::*;
#[component]
fn Counter(cx: Scope, initial_value: i32) -> Element {
fn Counter(cx: Scope, initial_value: i32) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial_value);
view! {
cx,
<div>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
<span>"Value: " {move || value.get().to_string()} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
</div>
}
@ -55,7 +49,7 @@ fn ssr_test_with_components() {
};
assert_eq!(
rendered,
rendered.into_view(cx).render_to_string(cx),
"<div data-hk=\"0-0\" class=\"counters\"><!--#--><div data-hk=\"0-2-0\"><button>-1</button><span>Value: <!--#-->1<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk=\"0-3-0\"><button>-1</button><span>Value: <!--#-->2<!--/-->!</span><button>+1</button></div><!--/--></div>"
);
});
@ -64,19 +58,17 @@ fn ssr_test_with_components() {
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn test_classes() {
use leptos_dom::*;
use leptos_macro::view;
use leptos_reactive::{create_runtime, create_scope, create_signal};
use leptos::*;
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 5);
let rendered = view! {
cx,
<div class="my big" class:a={move || value() > 10} class:red=true class:car={move || value() > 1}></div>
<div class="my big" class:a={move || value.get() > 10} class:red=true class:car={move || value.get() > 1}></div>
};
assert_eq!(
rendered,
rendered.into_view(cx).render_to_string(cx),
r#"<div data-hk="0-0" class="my big red car"></div>"#
);
});

View File

@ -28,6 +28,7 @@ use hydration::HydrationCtx;
pub use js_sys;
use leptos_reactive::Scope;
pub use logging::*;
pub use macro_helpers::{IntoClass, IntoAttribute, IntoProperty};
pub use node_ref::*;
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
use smallvec::SmallVec;

View File

@ -21,7 +21,8 @@ pub enum Attribute {
}
impl Attribute {
/// Converts the attribute to its HTML value at that moment so it can be rendered on the server.
/// Converts the attribute to its HTML value at that moment, including the attribute name,
/// so it can be rendered on the server.
pub fn as_value_string(&self, attr_name: &'static str) -> String {
match self {
Attribute::String(value) => format!("{attr_name}=\"{value}\""),
@ -45,6 +46,28 @@ impl Attribute {
}
}
}
/// Converts the attribute to its HTML value at that moment, not including
/// the attribute name, so it can be rendered on the server.
pub fn as_nameless_value_string(&self) -> String {
match self {
Attribute::String(value) => value.to_string(),
Attribute::Fn(_, f) => {
let mut value = f();
while let Attribute::Fn(_, f) = value {
value = f();
}
value.as_nameless_value_string()
}
Attribute::Option(_, value) => value
.as_ref()
.map(|value| value.to_string())
.unwrap_or_default(),
Attribute::Bool(_) => {
String::new()
}
}
}
}
impl PartialEq for Attribute {

View File

@ -1,6 +1,6 @@
mod into_attribute;
mod into_class;
mod into_property;
pub(crate) use into_attribute::*;
pub(crate) use into_class::*;
pub(crate) use into_property::*;
pub use into_attribute::*;
pub use into_class::*;
pub use into_property::*;

View File

@ -34,7 +34,7 @@ leptos = { path = "../leptos", version = "0.0" }
default = ["ssr"]
csr = ["leptos_dom/web", "leptos_reactive/csr"]
hydrate = ["leptos_dom/web", "leptos_reactive/hydrate"]
ssr = ["leptos_reactive/ssr"]
ssr = ["leptos_dom/ssr", "leptos_reactive/ssr"]
stable = ["leptos_dom/stable", "leptos_reactive/stable"]
[package.metadata.cargo-all-features]

View File

@ -18,11 +18,6 @@ pub(crate) enum Mode {
impl Default for Mode {
fn default() -> Self {
// what's the deal with this order of priority?
// basically, it's fine for the server to compile wasm-bindgen, but it will panic if it runs it
// for the sake of testing, we need to fall back to `ssr` if no flags are enabled
// if you have `hydrate` enabled, you definitely want that rather than `csr`
// if you have both `csr` and `ssr` we assume you want the browser
if cfg!(feature = "hydrate") || cfg!(feature = "csr") || cfg!(feature = "web") {
Mode::Client
} else {
@ -234,8 +229,8 @@ pub fn view(tokens: TokenStream) -> TokenStream {
&proc_macro2::Ident::new(&cx.to_string(), cx.span().into()),
&nodes,
// swap to Mode::default() to use faster SSR templating
Mode::Client
//Mode::default(),
//Mode::Client
Mode::default(),
),
Err(error) => error.to_compile_error(),
}

View File

@ -255,7 +255,7 @@ fn element_to_tokens_ssr(cx: &Ident, node: &NodeElement, template: &mut String,
}
}
// TODO: handle classes
set_class_attribute_ssr(cx, node, template, holes);
if is_self_closing(node) {
template.push_str("/>");
@ -343,10 +343,12 @@ fn attribute_to_tokens_ssr(cx: &Ident, node: &NodeAttribute, template: &mut Stri
// ignore classes: we'll handle these separately
} else {
let name = name.replacen("attr:", "", 1);
template.push(' ');
template.push_str(&name);
if let Some(value) = node.value.as_ref() {
if name != "class" {
template.push(' ');
template.push_str(&name);
if let Some(value) = node.value.as_ref() {
if let Some(value) = value_to_string(value) {
template.push_str(&value);
} else {
@ -358,10 +360,99 @@ fn attribute_to_tokens_ssr(cx: &Ident, node: &NodeAttribute, template: &mut Stri
})
}
template.push('"');
}
}
}
}
fn set_class_attribute_ssr(cx: &Ident, node: &NodeElement, template: &mut String, holes: &mut Vec<TokenStream>) {
let static_class_attr = node.attributes
.iter()
.filter_map(|a| if let Node::Attribute(a) = a {
if a.key.to_string() == "class" {
if let Some(value) = a.value.as_ref().and_then(|v| value_to_string(v)) {
Some(value)
} else {
None
}
} else {
None
}
} else {
None
})
.collect::<Vec<_>>()
.join(" ");
let dyn_class_attr = node.attributes
.iter()
.filter_map(|a| if let Node::Attribute(a) = a {
if a.key.to_string() == "class" {
if a.value.as_ref().and_then(value_to_string).is_some() {
None
} else {
Some((a.key.span(), &a.value))
}
} else {
None
}
} else {
None
})
.collect::<Vec<_>>();
let class_attrs = node.attributes
.iter()
.filter_map(|node| {
if let Node::Attribute(node) = node {
let name = node.key.to_string();
if name.starts_with("class:") || name.starts_with("class-") {
let name = if name.starts_with("class:") {
name.replacen("class:", "", 1)
} else if name.starts_with("class-") {
name.replacen("class-", "", 1)
} else {
name
};
let value = node.value.as_ref().expect("class: attributes need values").as_ref();
let span = node.key.span();
Some((span, name, value))
} else {
None
}
}
else {
None
}
})
.collect::<Vec<_>>();
if !static_class_attr.is_empty() || !dyn_class_attr.is_empty() || !class_attrs.is_empty() {
template.push_str(" class=\"");
template.push_str(&static_class_attr);
for (span, value) in dyn_class_attr {
if let Some(value) = value {
template.push_str(" {}");
let value = value.as_ref();
holes.push(quote_spanned! {
span => leptos::escape_attr(&(cx, #value).into_attribute(#cx).as_nameless_value_string()),
});
}
}
for (span, name, value) in &class_attrs {
template.push_str(" {}");
holes.push(quote_spanned! {
*span => (cx, #value).into_class(#cx).as_value_string(#name),
});
}
template.push('"');
}
}
fn fragment_to_tokens(
cx: &Ident,
span: Span,