The element! Macro #
The element! macro is the primary way to build component trees in eye-declare. It provides a JSX-like syntax that compiles down to Elements builder calls.
Basic syntax #
use eye_declare::{element, Elements};
fn my_view(state: &MyState) -> Elements {
element! {
TextBlock {
Line {
Span(text: "Hello, world!".into())
}
}
}
}
The macro returns an Elements value — a list of component descriptions that the framework uses to build and reconcile the component tree.
Components with props #
Props use Rust struct initialization syntax. The component must implement Default, and props are set as field assignments:
element! {
Spinner(label: "Loading...".into())
Markdown(source: "# Hello".into())
}
This is equivalent to:
let mut els = Elements::new();
els.add(Spinner { label: "Loading...".into(), ..Default::default() });
els.add(Markdown { source: "# Hello".into(), ..Default::default() });
els
Components with children #
Curly braces after a component provide its children:
element! {
VStack {
"First item"
"Second item"
Spinner(label: "Working...")
}
}
Children are collected into an Elements list and passed to the component's children() method as the slot parameter. The component must implement ChildCollector (use the impl_slot_children! macro for this).
Props and children together #
element! {
Card(title: "My Card".into()) {
Spinner(label: "Loading...")
"Some content"
}
}
String literals #
Bare string literals are automatically wrapped in TextBlock:
element! {
"This becomes a TextBlock"
"So does this"
}
Keys #
The key prop gives a component a stable identity for reconciliation. It's separated from regular props with a special syntax:
element! {
#(for item in &state.items {
Markdown(key: item.id.clone(), source: item.text.clone())
})
}
Keys are critical when rendering dynamic lists — without them, the framework matches components by position, which can cause state to "jump" between items when the list changes. See Reconciliation for details.
Conditionals #
Use #(if ...) for conditional rendering:
element! {
#(if state.loading {
Spinner(label: "Loading...")
})
#(if state.error.is_some() {
"An error occurred"
})
}
Pattern-matching conditionals with if let:
element! {
#(if let Some(ref result) = state.result {
Markdown(source: result.clone())
})
}
When the condition is false, no component is emitted — the framework handles the absence during reconciliation (unmounting the component if it previously existed).
Loops #
Use #(for ...) for iterating over collections:
element! {
#(for (i, msg) in state.messages.iter().enumerate() {
TextBlock(key: format!("msg-{i}")) {
Line {
Span(text: msg.clone())
}
}
})
}
Always provide keys when rendering lists so that the framework can correctly track which items were added, removed, or reordered.
Splicing #
Use #(expr) to splice a pre-built Elements value inline:
fn footer(state: &AppState) -> Elements {
element! {
"---"
TextBlock {
Line {
Span(text: format!("{} items", state.items.len()))
}
}
}
}
fn main_view(state: &AppState) -> Elements {
element! {
"Header"
#(for item in &state.items {
Markdown(key: item.id.clone(), source: item.text.clone())
})
#(footer(state))
}
}
This is useful for composing view functions — you can break your UI into smaller functions that each return Elements, then splice them together.
Syntax reference #
| Syntax | Description |
|---|---|
Component(prop: value) | Component with props (struct field init) |
Component { ... } | Component with children |
Component(props) { children } | Both props and children |
"text" | String literal — auto-wrapped as TextBlock |
key: expr | Special prop for stable identity across rebuilds |
#(if cond { ... }) | Conditional children |
#(if let pat = expr { ... }) | Pattern-matching conditional |
#(for pat in iter { ... }) | Loop children |
#(expr) | Splice a pre-built Elements value inline |
Without the macro #
You can build Elements imperatively if you prefer:
fn my_view(state: &MyState) -> Elements {
let mut els = Elements::new();
for msg in &state.messages {
els.add(Markdown::new(&msg.text)).key(msg.id.clone());
}
if state.loading {
els.add(Spinner::new("Loading...")).key("spinner");
}
els
}
The macro and the imperative API produce identical results — use whichever is clearer for your use case.