Merge pull request #351 from leptos-rs/view-styling

Add support for `class = ...`, in `view` macro to support scoped styling
This commit is contained in:
Greg Johnston 2023-01-21 12:56:21 -05:00 committed by GitHub
commit a75abb9e04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 192 additions and 34 deletions

View file

@ -73,3 +73,25 @@ fn test_classes() {
);
});
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn ssr_with_styles() {
use leptos::*;
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 0);
let styles = "myclass";
let rendered = view! {
cx, class = styles,
<div>
<button class="btn" on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
</div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div id=\"_0-1\" class=\" myclass\"><button id=\"_0-2\" class=\"btn myclass\">-1</button></div>"
);
});
}

View file

@ -4,7 +4,8 @@
#[macro_use]
extern crate proc_macro_error;
use proc_macro::{TokenStream, TokenTree};
use proc_macro::TokenStream;
use proc_macro2::TokenTree;
use quote::ToTokens;
use server::server_macro_impl;
use syn::{parse_macro_input, DeriveInput};
@ -196,7 +197,7 @@ mod server;
/// # });
/// ```
///
/// 8. You can use the `_ref` attribute to store a reference to its DOM element in a
/// 8. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
/// [NodeRef](leptos_reactive::NodeRef) to use later.
/// ```rust
/// # use leptos::*;
@ -211,6 +212,24 @@ mod server;
/// # });
/// ```
///
/// 9. You can add the same class to every element in the view by passing in a special
/// `class = {/* ... */}` argument after `cx, `. This is useful for injecting a class
/// providing by a scoped styling library.
/// ```rust
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let class = "mycustomclass";
/// view! { cx, class = class,
/// <div> // will have class="mycustomclass"
/// <p>"Some text"</p> // will also have class "mycustomclass"
/// </div>
/// }
/// # ;
/// # }
/// # });
/// ```
///
/// Heres a simple example that shows off several of these features, put together
/// ```rust
/// # use leptos::*;
@ -243,15 +262,45 @@ mod server;
#[proc_macro_error::proc_macro_error]
#[proc_macro]
pub fn view(tokens: TokenStream) -> TokenStream {
let tokens: proc_macro2::TokenStream = tokens.into();
let mut tokens = tokens.into_iter();
let (cx, comma) = (tokens.next(), tokens.next());
match (cx, comma) {
(Some(TokenTree::Ident(cx)), Some(TokenTree::Punct(punct))) if punct.as_char() == ',' => {
match parse(tokens.collect()) {
let first = tokens.next();
let second = tokens.next();
let third = tokens.next();
let fourth = tokens.next();
let global_class = match (&first, &second, &third, &fourth) {
(
Some(TokenTree::Ident(first)),
Some(TokenTree::Punct(eq)),
Some(val),
Some(TokenTree::Punct(comma)),
) if *first == "class"
&& eq.to_string() == '='.to_string()
&& comma.to_string() == ','.to_string() =>
{
Some(val.clone())
}
_ => None,
};
let tokens = if global_class.is_some() {
tokens.collect::<proc_macro2::TokenStream>()
} else {
[first, second, third, fourth]
.into_iter()
.flatten()
.chain(tokens)
.collect()
};
match parse(tokens.into()) {
Ok(nodes) => render_view(
&proc_macro2::Ident::new(&cx.to_string(), cx.span().into()),
&proc_macro2::Ident::new(&cx.to_string(), cx.span()),
&nodes,
Mode::default(),
global_class.as_ref(),
),
Err(error) => error.to_compile_error(),
}

View file

@ -1,4 +1,4 @@
use proc_macro2::{Ident, Span, TokenStream};
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use syn::{spanned::Spanned, Expr, ExprLit, ExprPath, Lit};
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName};
@ -142,7 +142,12 @@ const TYPED_EVENTS: [&str; 126] = [
"visibilitychange",
];
pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream {
pub(crate) fn render_view(
cx: &Ident,
nodes: &[Node],
mode: Mode,
global_class: Option<&TokenTree>,
) -> TokenStream {
if mode == Mode::Ssr {
if nodes.is_empty() {
let span = Span::call_site();
@ -150,9 +155,9 @@ pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream
span => leptos::Unit
}
} else if nodes.len() == 1 {
root_node_to_tokens_ssr(cx, &nodes[0])
root_node_to_tokens_ssr(cx, &nodes[0], global_class)
} else {
fragment_to_tokens_ssr(cx, Span::call_site(), nodes)
fragment_to_tokens_ssr(cx, Span::call_site(), nodes, global_class)
}
} else if nodes.is_empty() {
let span = Span::call_site();
@ -160,16 +165,27 @@ pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream
span => leptos::Unit
}
} else if nodes.len() == 1 {
node_to_tokens(cx, &nodes[0], TagType::Unknown)
node_to_tokens(cx, &nodes[0], TagType::Unknown, global_class)
} else {
fragment_to_tokens(cx, Span::call_site(), nodes, false, TagType::Unknown)
fragment_to_tokens(
cx,
Span::call_site(),
nodes,
false,
TagType::Unknown,
global_class,
)
}
}
fn root_node_to_tokens_ssr(cx: &Ident, node: &Node) -> TokenStream {
fn root_node_to_tokens_ssr(
cx: &Ident,
node: &Node,
global_class: Option<&TokenTree>,
) -> TokenStream {
match node {
Node::Fragment(fragment) => {
fragment_to_tokens_ssr(cx, Span::call_site(), &fragment.children)
fragment_to_tokens_ssr(cx, Span::call_site(), &fragment.children, global_class)
}
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
Node::Text(node) => {
@ -185,13 +201,18 @@ fn root_node_to_tokens_ssr(cx: &Ident, node: &Node) -> TokenStream {
#value
}
}
Node::Element(node) => root_element_to_tokens_ssr(cx, node),
Node::Element(node) => root_element_to_tokens_ssr(cx, node, global_class),
}
}
fn fragment_to_tokens_ssr(cx: &Ident, _span: Span, nodes: &[Node]) -> TokenStream {
fn fragment_to_tokens_ssr(
cx: &Ident,
_span: Span,
nodes: &[Node],
global_class: Option<&TokenTree>,
) -> TokenStream {
let nodes = nodes.iter().map(|node| {
let node = root_node_to_tokens_ssr(cx, node);
let node = root_node_to_tokens_ssr(cx, node, global_class);
quote! {
#node.into_view(#cx)
}
@ -205,9 +226,13 @@ fn fragment_to_tokens_ssr(cx: &Ident, _span: Span, nodes: &[Node]) -> TokenStrea
}
}
fn root_element_to_tokens_ssr(cx: &Ident, node: &NodeElement) -> TokenStream {
if is_component_node(&node) {
component_to_tokens(cx, node)
fn root_element_to_tokens_ssr(
cx: &Ident,
node: &NodeElement,
global_class: Option<&TokenTree>,
) -> TokenStream {
if is_component_node(node) {
component_to_tokens(cx, node, global_class)
} else {
let mut template = String::new();
let mut holes = Vec::<TokenStream>::new();
@ -220,6 +245,7 @@ fn root_element_to_tokens_ssr(cx: &Ident, node: &NodeElement) -> TokenStream {
&mut holes,
&mut exprs_for_compiler,
true,
global_class,
);
let template = if holes.is_empty() {
@ -253,10 +279,11 @@ fn element_to_tokens_ssr(
holes: &mut Vec<TokenStream>,
exprs_for_compiler: &mut Vec<TokenStream>,
is_root: bool,
global_class: Option<&TokenTree>,
) {
if is_component_node(node) {
template.push_str("{}");
let component = component_to_tokens(cx, node);
let component = component_to_tokens(cx, node, global_class);
holes.push(quote! {
{#component}.into_view(cx).render_to_string(cx),
})
@ -282,15 +309,15 @@ fn element_to_tokens_ssr(
.find(|node| matches!(node, Node::Attribute(attr) if attr.key.to_string() == "id"))
{
Some(_) => {
template.push_str(&format!(" leptos-hk=\"_{{}}\""));
template.push_str(" leptos-hk=\"_{}\"");
}
None => {
template.push_str(&format!(" id=\"_{{}}\""));
template.push_str(" id=\"_{}\"");
}
}
holes.push(hydration_id);
set_class_attribute_ssr(cx, node, template, holes);
set_class_attribute_ssr(cx, node, template, holes, global_class);
if is_self_closing(node) {
template.push_str("/>");
@ -298,9 +325,15 @@ fn element_to_tokens_ssr(
template.push('>');
for child in &node.children {
match child {
Node::Element(child) => {
element_to_tokens_ssr(cx, child, template, holes, exprs_for_compiler, false)
}
Node::Element(child) => element_to_tokens_ssr(
cx,
child,
template,
holes,
exprs_for_compiler,
false,
global_class,
),
Node::Text(text) => {
if let Some(value) = value_to_string(&text.value) {
template.push_str(&value);
@ -418,7 +451,17 @@ fn set_class_attribute_ssr(
node: &NodeElement,
template: &mut String,
holes: &mut Vec<TokenStream>,
global_class: Option<&TokenTree>,
) {
let static_global_class = match global_class {
Some(TokenTree::Literal(lit)) => lit.to_string(),
_ => String::new(),
};
let dyn_global_class = match global_class {
None => None,
Some(TokenTree::Literal(_)) => None,
Some(val) => Some(val),
};
let static_class_attr = node
.attributes
.iter()
@ -433,6 +476,8 @@ fn set_class_attribute_ssr(
None
}
})
.chain(std::iter::once(static_global_class))
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
@ -496,7 +541,11 @@ fn set_class_attribute_ssr(
})
.collect::<Vec<_>>();
if !static_class_attr.is_empty() || !dyn_class_attr.is_empty() || !class_attrs.is_empty() {
if !static_class_attr.is_empty()
|| !dyn_class_attr.is_empty()
|| !class_attrs.is_empty()
|| dyn_global_class.is_some()
{
template.push_str(" class=\"");
template.push_str(&static_class_attr);
@ -518,6 +567,11 @@ fn set_class_attribute_ssr(
});
}
if let Some(dyn_global_class) = dyn_global_class {
template.push_str(" {}");
holes.push(quote! { #dyn_global_class, });
}
template.push('"');
}
}
@ -528,9 +582,10 @@ fn fragment_to_tokens(
nodes: &[Node],
lazy: bool,
parent_type: TagType,
global_class: Option<&TokenTree>,
) -> TokenStream {
let nodes = nodes.iter().map(|node| {
let node = node_to_tokens(cx, node, parent_type);
let node = node_to_tokens(cx, node, parent_type, global_class);
quote! {
#node.into_view(#cx)
@ -555,7 +610,12 @@ fn fragment_to_tokens(
}
}
fn node_to_tokens(cx: &Ident, node: &Node, parent_type: TagType) -> TokenStream {
fn node_to_tokens(
cx: &Ident,
node: &Node,
parent_type: TagType,
global_class: Option<&TokenTree>,
) -> TokenStream {
match node {
Node::Fragment(fragment) => fragment_to_tokens(
cx,
@ -563,6 +623,7 @@ fn node_to_tokens(cx: &Ident, node: &Node, parent_type: TagType) -> TokenStream
&fragment.children,
false,
parent_type,
global_class,
),
Node::Comment(_) | Node::Doctype(_) => quote! {},
Node::Text(node) => {
@ -576,13 +637,18 @@ fn node_to_tokens(cx: &Ident, node: &Node, parent_type: TagType) -> TokenStream
quote! { #value }
}
Node::Attribute(node) => attribute_to_tokens(cx, node),
Node::Element(node) => element_to_tokens(cx, node, parent_type),
Node::Element(node) => element_to_tokens(cx, node, parent_type, global_class),
}
}
fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -> TokenStream {
fn element_to_tokens(
cx: &Ident,
node: &NodeElement,
mut parent_type: TagType,
global_class: Option<&TokenTree>,
) -> TokenStream {
if is_component_node(node) {
component_to_tokens(cx, node)
component_to_tokens(cx, node, global_class)
} else {
let tag = node.name.to_string();
let name = if is_custom_element(&tag) {
@ -623,6 +689,14 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
None
}
});
let global_class_expr = match global_class {
None => quote! {},
Some(class) => {
quote! {
.class(#class, true)
}
}
};
let children = node.children.iter().map(|node| {
let child = match node {
Node::Fragment(fragment) => fragment_to_tokens(
@ -631,6 +705,7 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
&fragment.children,
false,
parent_type,
global_class,
),
Node::Text(node) => {
let value = node.value.as_ref();
@ -644,7 +719,7 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
#[allow(unused_braces)] #value
}
}
Node::Element(node) => element_to_tokens(cx, node, parent_type),
Node::Element(node) => element_to_tokens(cx, node, parent_type, global_class),
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
};
quote! {
@ -654,6 +729,7 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
quote! {
#name
#(#attrs)*
#global_class_expr
#(#children)*
}
}
@ -830,7 +906,11 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
}
}
fn component_to_tokens(cx: &Ident, node: &NodeElement) -> TokenStream {
fn component_to_tokens(
cx: &Ident,
node: &NodeElement,
global_class: Option<&TokenTree>,
) -> TokenStream {
let name = &node.name;
let component_name = ident_from_tag_name(&node.name);
let span = node.name.span();
@ -881,7 +961,14 @@ fn component_to_tokens(cx: &Ident, node: &NodeElement) -> TokenStream {
let children = if node.children.is_empty() {
quote! {}
} else {
let children = fragment_to_tokens(cx, span, &node.children, true, TagType::Unknown);
let children = fragment_to_tokens(
cx,
span,
&node.children,
true,
TagType::Unknown,
global_class,
);
let clonables = items_to_clone
.iter()