Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plait-macros/src/ast/component_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ pub struct ComponentCall {

pub struct ComponentCallField {
pub ident: Ident,
pub value: Expr,
pub value: Option<Expr>,
}
11 changes: 8 additions & 3 deletions plait-macros/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand Down
13 changes: 9 additions & 4 deletions plait-macros/src/parse/component_call.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use syn::{
braced, parenthesized,
Ident, braced, parenthesized,
parse::{Parse, ParseStream},
token::{At, Colon, Comma, Paren, Semi},
};
Expand Down Expand Up @@ -70,9 +70,14 @@ impl Parse for ComponentCall {

impl Parse for ComponentCallField {
fn parse(input: ParseStream) -> syn::Result<Self> {
let ident = input.parse()?;
let _ = input.parse::<Colon>()?;
let value = input.parse()?;
let ident: Ident = input.parse()?;

let value = if input.peek(Colon) {
let _ = input.parse::<Colon>()?;
input.parse().map(Some)
} else {
Ok(None)
}?;

Ok(Self { ident, value })
}
Expand Down
100 changes: 100 additions & 0 deletions plait/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(), "<div><span>Alice</span> - <span>Admin</span></div>");
```

### Passing fragments as props

Use `PartialHtml` as a prop bound to accept `html!` output as a component prop:
Expand Down Expand Up @@ -295,6 +319,82 @@ assert_eq!(frag.to_html(), r#"<div class="base primary"></div>"#);
Values passed to `classes!` must implement the `Class` trait. This is implemented for `&str`, `Option<T>` where
`T: Class`, and `Classes<T>`(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
Expand Down
61 changes: 61 additions & 0 deletions plait/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(), "<div><span>Alice</span> - <span>Admin</span></div>");
//! ```
//!
//! ## Passing fragments as props
//!
//! Use [`PartialHtml`] as a prop bound to accept [`html!`] output as a component prop:
Expand Down Expand Up @@ -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(), "<button class=\"btn primary\">Click</button>");
/// ```
pub use plait_macros::component;

pub use self::{
Expand Down
128 changes: 127 additions & 1 deletion plait/tests/component_macro_tests.rs
Original file line number Diff line number Diff line change
@@ -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>) {
Expand Down Expand Up @@ -50,3 +50,129 @@ fn test_card() {
"<div class=\"card\"><h1><span>My card</span></h1><button class=\"btn btn-primary\" disabled>Click me</button></div>"
);
}

// --- 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(), "<span>Hello, Alice!</span>");
}

#[test]
fn test_shorthand_multiple_fields() {
let name = "Alice";
let role = "Admin";

let html = html! {
@UserCard(name, role) {}
};

assert_eq!(
html.to_html(),
"<div class=\"user-card\"><span class=\"name\">Alice</span><span class=\"role\">Admin</span></div>"
);
}

#[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(),
"<a href=\"https://example.com/\" class=\"link\">My Link</a>"
);
}

#[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(),
"<a href=\"https://example.com/\" class=\"\" id=\"my-link\">Click</a>"
);
}

#[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(), "<button class=\"btn\">Click me</button>");
assert_eq!(
html2.to_html(),
"<button class=\"btn primary\">Submit</button>"
);
}