diff --git a/plait-macros/src/ast/component_call.rs b/plait-macros/src/ast/component_call.rs index face20a..5dc9e39 100644 --- a/plait-macros/src/ast/component_call.rs +++ b/plait-macros/src/ast/component_call.rs @@ -11,5 +11,5 @@ pub struct ComponentCall { pub struct ComponentCallField { pub ident: Ident, - pub value: Expr, + pub value: Option, } diff --git a/plait-macros/src/buffer.rs b/plait-macros/src/buffer.rs index d5025dc..9769464 100644 --- a/plait-macros/src/buffer.rs +++ b/plait-macros/src/buffer.rs @@ -395,9 +395,14 @@ impl InnerBuffer { let ident = &field.ident; let value = &field.value; - field_statements.push(quote! { - #ident : #value - }); + match value { + Some(value) => field_statements.push(quote! { + #ident : #value + }), + None => field_statements.push(quote! { + #ident + }), + } } let component_statement = quote! { diff --git a/plait-macros/src/parse/component_call.rs b/plait-macros/src/parse/component_call.rs index cd4ba53..a44cc5d 100644 --- a/plait-macros/src/parse/component_call.rs +++ b/plait-macros/src/parse/component_call.rs @@ -1,5 +1,5 @@ use syn::{ - braced, parenthesized, + Ident, braced, parenthesized, parse::{Parse, ParseStream}, token::{At, Colon, Comma, Paren, Semi}, }; @@ -70,9 +70,14 @@ impl Parse for ComponentCall { impl Parse for ComponentCallField { fn parse(input: ParseStream) -> syn::Result { - let ident = input.parse()?; - let _ = input.parse::()?; - let value = input.parse()?; + let ident: Ident = input.parse()?; + + let value = if input.peek(Colon) { + let _ = input.parse::()?; + input.parse().map(Some) + } else { + Ok(None) + }?; Ok(Self { ident, value }) } diff --git a/plait/README.md b/plait/README.md index 1b40a72..dab90fd 100644 --- a/plait/README.md +++ b/plait/README.md @@ -243,6 +243,30 @@ assert_eq!( In the component call, props appear before the `;`, and extra HTML attributes appear after. The component body uses `#attrs` to spread those extra attributes and `#children` to render the child content. +### Shorthand props + +When a variable has the same name as a component prop, you can use shorthand syntax - just like Rust struct +initialization: + +```rust +let class = "primary"; + +// These are equivalent: +let a = html! { @Button(class: class) { "Click" } }; +let b = html! { @Button(class) { "Click" } }; + +assert_eq!(a.to_html(), b.to_html()); +``` + +Shorthand and explicit props can be mixed freely: + +```rust +let name = "Alice"; +let html = html! { @UserCard(name, role: "Admin") {} }; + +assert_eq!(html.to_html(), "
Alice - Admin
"); +``` + ### Passing fragments as props Use `PartialHtml` as a prop bound to accept `html!` output as a component prop: @@ -295,6 +319,82 @@ assert_eq!(frag.to_html(), r#"
"#); Values passed to `classes!` must implement the `Class` trait. This is implemented for `&str`, `Option` where `T: Class`, and `Classes`(Classes). +## Web framework integrations + +Plait provides optional integrations with popular Rust web frameworks. Both `Html` and `HtmlFragment` can be +returned directly from request handlers when the corresponding feature is enabled. + +Enable integrations by adding the feature flag to your `Cargo.toml`: + +```toml +[dependencies] +plait = { version = "0.8", features = ["axum"] } +``` + +Available features: `actix-web`, `axum`, `rocket`. + +### axum + +`Html` and `HtmlFragment` implement +`IntoResponse`(https://docs.rs/axum/latest/axum/response/trait.IntoResponse.html): + +```rust +use axum::{Router, routing::get}; +use plait::{html, ToHtml}; + +async fn index() -> plait::Html { + html! { + h1 { "Hello from plait!" } + }.to_html() +} + +let app = Router::new().route("/", get(index)); +``` + +You can also return an `HtmlFragment` directly without calling `.to_html()`: + +```rust +async fn index() -> impl axum::response::IntoResponse { + plait::html! { + h1 { "Hello from plait!" } + } +} +``` + +### actix-web + +`Html` and `HtmlFragment` implement +`Responder`(https://docs.rs/actix-web/latest/actix_web/trait.Responder.html): + +```rust +use actix_web::{App, HttpServer, get}; +use plait::{html, ToHtml}; + +#[get("/")] +async fn index() -> plait::Html { + html! { + h1 { "Hello from plait!" } + }.to_html() +} +``` + +### rocket + +`Html` and `HtmlFragment` implement +`Responder`(https://docs.rs/rocket/latest/rocket/response/trait.Responder.html): + +```rust +use rocket::get; +use plait::{html, ToHtml}; + +#[get("/")] +fn index() -> plait::Html { + html! { + h1 { "Hello from plait!" } + }.to_html() +} +``` + ## License Licensed under either of diff --git a/plait/src/lib.rs b/plait/src/lib.rs index e3479cf..3aa34b5 100644 --- a/plait/src/lib.rs +++ b/plait/src/lib.rs @@ -268,6 +268,44 @@ //! In the component call, props appear before the `;`, and extra HTML attributes appear after. The component body uses //! `#attrs` to spread those extra attributes and `#children` to render the child content. //! +//! ## Shorthand props +//! +//! When a variable has the same name as a component prop, you can use shorthand syntax - just like Rust struct +//! initialization: +//! +//! ``` +//! # use plait::{component, html, ToHtml, classes, Class}; +//! # component! { +//! # pub fn Button(class: impl Class) { +//! # button(class: classes!("btn", class), #attrs) { +//! # #children +//! # } +//! # } +//! # } +//! let class = "primary"; +//! +//! // These are equivalent: +//! let a = html! { @Button(class: class) { "Click" } }; +//! let b = html! { @Button(class) { "Click" } }; +//! +//! assert_eq!(a.to_html(), b.to_html()); +//! ``` +//! +//! Shorthand and explicit props can be mixed freely: +//! +//! ``` +//! # use plait::{component, html, ToHtml}; +//! # component! { +//! # pub fn UserCard(name: &str, role: &str) { +//! # div { span { (name) } " - " span { (role) } } +//! # } +//! # } +//! let name = "Alice"; +//! let html = html! { @UserCard(name, role: "Admin") {} }; +//! +//! assert_eq!(html.to_html(), "
Alice - Admin
"); +//! ``` +//! //! ## Passing fragments as props //! //! Use [`PartialHtml`] as a prop bound to accept [`html!`] output as a component prop: @@ -502,6 +540,29 @@ pub use plait_macros::html; /// ``` /// /// Props go before `;`, extra HTML attributes go after. +/// +/// ## Shorthand props +/// +/// When a variable has the same name as a prop, you can omit the value - just like Rust struct initialization +/// shorthand: +/// +/// ``` +/// # use plait::{component, html, classes, Class, ToHtml}; +/// # component! { +/// # pub fn Button(class: impl Class) { +/// # button(class: classes!("btn", class), #attrs) { +/// # #children +/// # } +/// # } +/// # } +/// let class = "primary"; +/// +/// let html = html! { +/// @Button(class) { "Click" } +/// }; +/// +/// assert_eq!(html.to_html(), ""); +/// ``` pub use plait_macros::component; pub use self::{ diff --git a/plait/tests/component_macro_tests.rs b/plait/tests/component_macro_tests.rs index 79e29e3..9d1c381 100644 --- a/plait/tests/component_macro_tests.rs +++ b/plait/tests/component_macro_tests.rs @@ -1,4 +1,4 @@ -use plait::{RenderEscaped, ToHtml, classes, component, html}; +use plait::{Class, RenderEscaped, ToHtml, classes, component, html}; component! { pub fn Button<'a>(class: Option<&'a str>) { @@ -50,3 +50,129 @@ fn test_card() { "

My card

" ); } + +// --- Shorthand argument tests --- + +component! { + pub fn Link(href: &str, class: impl Class) { + a(href: href, class: classes!(class), #attrs) { + #children + } + } +} + +component! { + pub fn Greeting(name: &str) { + span { "Hello, " (name) "!" } + } +} + +component! { + pub fn UserCard(name: &str, role: &str) { + div(class: "user-card") { + span(class: "name") { (name) } + span(class: "role") { (role) } + } + } +} + +#[test] +fn test_shorthand_single_field() { + let name = "Alice"; + + let html = html! { + @Greeting(name) {} + }; + + assert_eq!(html.to_html(), "Hello, Alice!"); +} + +#[test] +fn test_shorthand_multiple_fields() { + let name = "Alice"; + let role = "Admin"; + + let html = html! { + @UserCard(name, role) {} + }; + + assert_eq!( + html.to_html(), + "
AliceAdmin
" + ); +} + +#[test] +fn test_shorthand_mixed_with_explicit() { + let href = "https://example.com/"; + + let html = html! { + @Link(href, class: Some("link")) { + "My Link" + } + }; + + assert_eq!( + html.to_html(), + "My Link" + ); +} + +#[test] +fn test_shorthand_with_attributes() { + let href = "https://example.com/"; + let class: Option<&str> = None; + + let html = html! { + @Link(href, class; id: "my-link") { + "Click" + } + }; + + assert_eq!( + html.to_html(), + "Click" + ); +} + +#[test] +fn test_shorthand_equivalent_to_explicit() { + let name = "Bob"; + let role = "User"; + + let shorthand = html! { + @UserCard(name, role) {} + }; + + let explicit = html! { + @UserCard(name: name, role: role) {} + }; + + assert_eq!(shorthand.to_html(), explicit.to_html()); +} + +#[test] +fn test_shorthand_with_ref_lifetime() { + let label = "Click me"; + + let html = html! { + @Button(class: None) { + (label) + } + }; + + // Also test shorthand with Option field + let class: Option<&str> = Some("primary"); + + let html2 = html! { + @Button(class) { + "Submit" + } + }; + + assert_eq!(html.to_html(), ""); + assert_eq!( + html2.to_html(), + "" + ); +}