DEV Community

tengxgfyrz67s
tengxgfyrz67s

Posted on

Building-Components

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:

  1. A Props struct that defines the data your component accepts
  2. 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,
}
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's break this down:

  1. #[component] — This attribute marks the function as a component, enabling it to receive props and children through the VirtualNode wrapper.

  2. node: VirtualNode<MyCardProps> — The function receives a VirtualNode parameterized with the props type. This node carries both the props and any child nodes.

  3. node.try_get_props() — This method attempts to extract the typed props from the virtual node. The unwrap_or_default() call provides a default value if extraction fails (e.g., when the component is used without props).

  4. 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.

  5. html! { ... } — The component returns a VirtualNode tree 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." }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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!"
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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" }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
                }))
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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:" }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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" }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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], accept VirtualNode<Props>, return VirtualNode
  • 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 Signal for two-way binding
  • watch!: Use watch! for cross-component reactive bindings
  • Keep-Alive: Use CSS display: none/block to 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)