Skip to content
Damask

Props

Fields are props: passing, skipping, moving, and inference.


A component’s struct fields are its props. A component tag names the props the author wrote and is compiled into a builder that constructs the struct, so ordinary Rust rules apply to the values.

Rust
#[derive(Component)]
pub struct Notice {
    pub title: String,
    pub detail: Option<String>,
    pub dismissible: Option<bool>,
}
Damask
<Notice title="Deploy finished"/>
<Notice title="Rollback" detail="check the log" dismissible/>

Skippable props

A prop must be passed unless its type says what leaving it out means, and only Option<_> does. A skipped prop arrives as None.

The check is syntactic, on the last segment of the path — std::option::Option<T> counts, a type alias for it does not.

Note

bool is required like anything else. A flag a caller may omit is Option<bool>. A bare dismissible at the call site is passed as true.into(), so it reaches either.

#[component(default)] on the struct makes every prop skippable, filling from Default — see the derive.

Forgetting a required prop is a trait-bound error at the call site that names it:

text
missing a required prop: __DamaskNotice_title

Quoted values convert; braced values do not

At the call site Reaches an Option<String> prop as
detail="check the log" Some("check the log".into())
detail="row {self.n}" Some("row 3".into())
detail={self.detail.clone()} exactly that Option
detail={Some(x)} Some(x)

A { … } value is the prop’s type, including its Option. That exactness is what makes inference work: a setter takes the prop’s type exactly, as assigning to the field did, so count={2 + 8} infers to whatever integer type the prop is and items={&self.rows} coerces a &Vec<T> to a &[T] prop.

The conversion therefore happens on the value side instead. An interpolated value is already a String and reaches both a String and an Option<String> prop through Into. Static text is the case Into cannot serve — no From<&'static str> for Option<String> exists — so damask::props::literal stands in, inferring which conversion it needs from the prop:

Prop type What a quoted value needs
T T: From<&'static str>
Option<T> T: From<&'static str> + From<String>

String, Cow<'static, str>, Box<str>, Rc<str>, Arc<str> and any type of your own with both conversions qualify. Option<&'static str> works too, through the first row. A prop a quoted value cannot reach reports so:

text
a quoted attribute value cannot become `…`

Values move

Attribute values move into the component’s fields, so a &self field is passed by clone — or is Copy:

Damask
<Card title={self.heading.clone()} tone={self.tone}/>

This is ordinary Rust ownership, not a template rule. If cloning a large value per render matters, make the prop a borrow and give the component a lifetime; the derive leaves generics alone, and the builder carries them:

Rust
#[derive(Component)]
pub struct Tagged<'a, T: Display> {
    pub value: T,
    pub note: Option<&'a str>,
}

Props that are not independent are worth one type rather than five fields. A button’s type, name, value, disabled and confirm all describe the same thing, and most call sites set none of them:

Rust
#[derive(Debug, Clone, Default)]
pub struct Act { /* … */ }

impl Act {
    pub fn inert() -> Self {}
    pub fn submit() -> Self {}
    pub fn confirm(mut self, message: impl Into<String>) -> Self {}
}

#[derive(Component)]
pub struct Button {
    pub act: Act,
    pub class: String,
}
Damask
<Button act={Act::submit().confirm("Reboot the router?")} class="">Reboot</Button>

The shape of the call site then says what the button is for, and no template ever spreads a Default.

What props are not

Attributes a component does not declare are not props. They are refused, unless the component opts into a bag for them with a #[prop(rest)] field — which is also what makes a prop the way a call site overrides something the component writes itself. See Attributes a component does not name.

Slots are not props — a template adds or drops a <slot> without the struct changing. See Slots.

There is also no ambient context: no request object, thread-local or implicit scope. Everything a component needs arrives as a prop. In a real application this pushes you toward a single “shell” value assembled once and passed down — which has the useful property that what a page depends on is visible in its type.

The props module

damask::props holds the typestate the builder is made of — Set, Provided, FromText, FromLiteral, literal — and Rest, the channel an attribute that is not a prop travels through. It exists to phrase the diagnostics above. Nothing in it is meant to be named by hand.