Building Components
Project Code:https://github.com/euv-dev/euv
Introduction to Components
Components are the building blocks of any euv application. They let you split the UI into independent, reusable pieces, each with its own logic and presentation. In euv, components are functions that take a VirtualNode (carrying props and children) and return a VirtualNode (describing the rendered output).
Defining a Component
To define a component in euv, you need two things:
- A Props struct that defines the data your component accepts
- A component function decorated with the
#[component]attribute
The Props Struct
The props struct defines the shape of the data your component receives. It must derive Clone and Default:
#[derive(Clone, Default)]
struct MyCardProps {
title: &'static str,
}
Props can be of various types, including:
-
&'static str— Static string slices, ideal for fixed text -
String— Owned strings for dynamic text -
bool— Boolean values for flags -
Signal<bool>— Reactive boolean signals -
i32,f64— Numeric types -
Option<Rc<dyn Fn(Event)>>— Optional callback functions -
Signal<T>— Reactive signals of any type -
VirtualNode— Child virtual nodes -
Css— CSS styling values
The Component Function
The component function takes a VirtualNode<YourProps> and returns a VirtualNode:
#[component]
pub fn my_card(node: VirtualNode<MyCardProps>) -> VirtualNode {
let MyCardProps { title, .. } = node.try_get_props().unwrap_or_default();
let children: VirtualNode = node.try_get_child_node();
html! {
div {
h3 { title }
children
}
}
}
Let's break this down:
#[component]— This attribute marks the function as a component, enabling it to receive props and children through theVirtualNodewrapper.node: VirtualNode<MyCardProps>— The function receives aVirtualNodeparameterized with the props type. This node carries both the props and any child nodes.node.try_get_props()— This method attempts to extract the typed props from the virtual node. Theunwrap_or_default()call provides a default value if extraction fails (e.g., when the component is used without props).node.try_get_child_node()— This method extracts the child nodes that were passed to the component. These can be rendered inside the component's template.html! { ... }— The component returns aVirtualNodetree describing what to render.
Using Components
Once defined, components can be used inside the html! macro by referencing their function name:
fn app() -> VirtualNode {
html! {
div {
MyCard {
title: "Welcome to euv"
p { "This content is passed as children." }
}
}
}
}
Props are specified as named attributes (title: "Welcome to euv"), and child nodes are nested inside the component's curly braces.
Component Nesting
Components can be nested inside other components to build complex UIs:
#[derive(Clone, Default)]
struct AlertProps {
message: &'static str,
}
#[component]
pub fn alert(node: VirtualNode<AlertProps>) -> VirtualNode {
let AlertProps { message, .. } = node.try_get_props().unwrap_or_default();
html! {
div {
p { message }
}
}
}
#[derive(Clone, Default)]
struct DashboardProps {
username: &'static str,
}
#[component]
pub fn dashboard(node: VirtualNode<DashboardProps>) -> VirtualNode {
let DashboardProps { username, .. } = node.try_get_props().unwrap_or_default();
html! {
div {
h1 { username }
Alert {
message: "Welcome to your dashboard!"
}
}
}
}
In this example, the dashboard component nests the alert component inside its template. This composability is one of the key strengths of the component system.
Passing Children to Components
Children are a powerful pattern for creating wrapper components. The parent passes content, and the child component decides where to render it:
#[derive(Clone, Default)]
struct CardProps {
title: &'static str,
}
#[component]
pub fn card(node: VirtualNode<CardProps>) -> VirtualNode {
let CardProps { title, .. } = node.try_get_props().unwrap_or_default();
let children: VirtualNode = node.try_get_child_node();
html! {
div {
h3 { title }
div {
children
}
}
}
}
fn app() -> VirtualNode {
html! {
Card {
title: "My Card"
p { "This is the card body." }
button { onclick: move |event: Event| { /* handler */ } "Action" }
}
}
}
The card component receives the title prop and the child nodes (p and button), then renders them in its own layout.
Props Down / Callback Up
euv follows the "Props Down, Callback Up" pattern:
- Props Down: Parent components pass data to child components via props
- Callback Up: Child components communicate back to parents via callback functions
#[derive(Clone, Default)]
struct ChildProps {
on_click: Option<Rc<dyn Fn(Event)>>,
}
#[component]
pub fn child(node: VirtualNode<ChildProps>) -> VirtualNode {
let ChildProps { on_click, .. } = node.try_get_props().unwrap_or_default();
html! {
button {
onclick: on_click
"Click me"
}
}
}
fn app() -> VirtualNode {
let count: Signal<i32> = use_signal(|| 0);
html! {
div {
Child {
on_click: Some(Rc::new(move |event: Event| {
count.set(count.get() + 1);
}))
}
}
}
}
Shared Signals for Two-Way Binding
For true two-way binding between parent and child, you can share a Signal directly:
#[derive(Clone, Default)]
struct InputProps {
value: Signal<String>,
}
#[component]
pub fn text_input(node: VirtualNode<InputProps>) -> VirtualNode {
let InputProps { value, .. } = node.try_get_props().unwrap_or_default();
html! {
input {
oninput: on_input_value(value)
}
}
}
fn app() -> VirtualNode {
let name: Signal<String> = use_signal(|| "".to_string());
html! {
div {
TextInput {
value: name
}
p { "You typed:" }
}
}
}
By sharing the name signal, both the parent and the child can read and write to the same state, achieving two-way binding.
Watch for Cross-Component Reactivity
The watch! macro enables cross-component reactive bindings by watching a signal and running a callback when it changes:
fn app() -> VirtualNode {
let celsius: Signal<f64> = use_signal(|| 0.0);
let fahrenheit: Signal<f64> = use_signal(|| 32.0);
watch!(celsius, |celsius_value: f64| {
fahrenheit.set(celsius_value * 9.0 / 5.0 + 32.0);
});
html! {
div {
input {
oninput: on_input_value(celsius)
}
p { "Fahrenheit" }
}
}
}
Here, watch! monitors the celsius signal and automatically updates fahrenheit whenever celsius changes. This pattern is useful for keeping multiple pieces of state in sync.
Keep-Alive Pattern
Sometimes you want to hide a component without unmounting it (preserving its state). euv supports this through the Keep-Alive pattern using CSS display: none/block:
fn app() -> VirtualNode {
let show: Signal<bool> = use_signal(|| true);
html! {
div {
if { show.get() } {
div { class: euv-fade-in "Content visible" }
} else {
div { style: "display: none" "Content hidden but alive" }
}
button {
onclick: use_toggle(show)
"Toggle"
}
}
}
}
Instead of conditionally rendering (which would unmount and lose state), you can use CSS to toggle visibility while keeping the component alive.
Summary
Components are the foundation of euv application architecture:
-
Props struct: Define with
#[derive(Clone, Default)]and typed fields -
Component function: Decorate with
#[component], acceptVirtualNode<Props>, returnVirtualNode -
Extract props: Use
node.try_get_props().unwrap_or_default() -
Extract children: Use
node.try_get_child_node() -
Component nesting: Use components inside
html!macro with named props and nested children - Props Down / Callback Up: Pass data down via props, communicate up via callbacks
-
Shared Signals: Share a
Signalfor two-way binding -
watch!: Use
watch!for cross-component reactive bindings -
Keep-Alive: Use CSS
display: none/blockto hide without unmounting
With these patterns, you can build complex, maintainable, and reusable UI applications in euv.
Project Code:https://github.com/euv-dev/euv
Top comments (0)