From d1333a3402846002e45c8589ea542fc50cf88183 Mon Sep 17 00:00:00 2001 From: jclmnop Date: Sun, 22 Jan 2023 12:37:40 +0000 Subject: [PATCH 1/2] modify component attribute macro to allow snake_case fn names --- .gitignore | 1 + leptos/tests/ssr.rs | 34 ++++++++++++++++++++++++++++++++++ leptos_macro/Cargo.toml | 1 + leptos_macro/src/component.rs | 12 +++++++++++- 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fbe21a725..d6359fc3d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ blob.rs Cargo.lock **/*.rs.bk .DS_Store +.idea diff --git a/leptos/tests/ssr.rs b/leptos/tests/ssr.rs index d9bdc3d20..ddf1bdb7d 100644 --- a/leptos/tests/ssr.rs +++ b/leptos/tests/ssr.rs @@ -55,6 +55,40 @@ fn ssr_test_with_components() { }); } +#[cfg(not(any(feature = "csr", feature = "hydrate")))] +#[test] +fn ssr_test_with_snake_case_components() { + use leptos::*; + + #[component] + fn snake_case_counter(cx: Scope, initial_value: i32) -> impl IntoView { + let (value, set_value) = create_signal(cx, initial_value); + view! { + cx, +
+ + "Value: " {move || value.get().to_string()} "!" + +
+ } + } + + _ = create_scope(create_runtime(), |cx| { + let rendered = view! { + cx, +
+ + +
+ }; + + assert_eq!( + rendered.into_view(cx).render_to_string(cx), + "
Value: 1!
Value: 2!
" + ); + }); +} + #[cfg(not(any(feature = "csr", feature = "hydrate")))] #[test] fn test_classes() { diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index 9c3d7d196..61be4d48e 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -27,6 +27,7 @@ leptos_dom = { workspace = true } leptos_reactive = { workspace = true } leptos_server = { workspace = true } lazy_static = "1.4" +convert_case = "0.6.0" [dev-dependencies] log = "0.4" diff --git a/leptos_macro/src/component.rs b/leptos_macro/src/component.rs index d1c5dbd0f..46784213b 100644 --- a/leptos_macro/src/component.rs +++ b/leptos_macro/src/component.rs @@ -1,3 +1,4 @@ +use convert_case::{Case::{Snake, Pascal}, Casing}; use itertools::Itertools; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, ToTokens, TokenStreamExt}; @@ -75,7 +76,7 @@ impl Parse for Model { is_transparent: false, docs, vis: item.vis.clone(), - name: item.sig.ident.clone(), + name: convert_from_snake_case(&item.sig.ident), scope_name, props, ret: item.sig.output.clone(), @@ -97,6 +98,15 @@ fn drain_filter(vec: &mut Vec, mut some_predicate: impl FnMut(&mut T) -> b } } +fn convert_from_snake_case(name: &Ident) -> Ident { + let name_str = name.to_string(); + if !name_str.is_case(Snake) { + name.clone() + } else { + Ident::new(&*name_str.to_case(Pascal), name.span().clone()) + } +} + impl ToTokens for Model { fn to_tokens(&self, tokens: &mut TokenStream) { let Self { From 39cddfc82d70d80f85a7796048c6463e794ccd76 Mon Sep 17 00:00:00 2001 From: jclmnop Date: Sun, 22 Jan 2023 15:28:15 +0000 Subject: [PATCH 2/2] update docs for component macro --- leptos_macro/src/component.rs | 5 ++++- leptos_macro/src/lib.rs | 26 ++++++++++++++++++++------ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/leptos_macro/src/component.rs b/leptos_macro/src/component.rs index 46784213b..bffd48eac 100644 --- a/leptos_macro/src/component.rs +++ b/leptos_macro/src/component.rs @@ -1,4 +1,7 @@ -use convert_case::{Case::{Snake, Pascal}, Casing}; +use convert_case::{ + Case::{Pascal, Snake}, + Casing, +}; use itertools::Itertools; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, ToTokens, TokenStreamExt}; diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs index 5b84b12ff..f45e7ac55 100644 --- a/leptos_macro/src/lib.rs +++ b/leptos_macro/src/lib.rs @@ -324,16 +324,18 @@ pub fn view(tokens: TokenStream) -> TokenStream { /// to do relatively expensive work within the component function, as it will only happen once, /// not on every state change. /// -/// 2. The component name should be `CamelCase` instead of `snake_case`. This is how the renderer -/// recognizes that a particular tag is a component, not an HTML element. +/// 2. If a `snake_case` name is used, then the generated component's name will still be in +/// `CamelCase`. This is how the renderer recognizes that a particular tag is a component, not +/// an HTML element. It's important to be aware of this when using or importing the component. /// /// ``` /// # use leptos::*; -/// // ❌ not snake_case -/// #[component] -/// fn my_component(cx: Scope) -> impl IntoView { todo!() } /// -/// // ✅ CamelCase +/// // snake_case: Generated component will be called MySnakeCaseComponent +/// #[component] +/// fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() } +/// +/// // CamelCase: Generated component will be called MyComponent /// #[component] /// fn MyComponent(cx: Scope) -> impl IntoView { todo!() } /// ``` @@ -354,6 +356,18 @@ pub fn view(tokens: TokenStream) -> TokenStream { /// pub fn MyComponent(cx: Scope) -> impl IntoView { todo!() } /// } /// ``` +/// ``` +/// # use leptos::*; +/// +/// use snake_case_component::{MySnakeCaseComponent, MySnakeCaseComponentProps}; +/// +/// mod snake_case_component { +/// use leptos::*; +/// +/// #[component] +/// pub fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() } +/// } +/// ``` /// /// 4. You can pass generic arguments, but they should be defined in a `where` clause and not inline. ///