Components

Introduction

A ReeTag is a server-side component invoked with the same hyphenated-name syntax the browser uses for custom web components - think of them as Server Side Custom Elements. A ReeTag is just a .ree file in the components/ directory: no class, no lifecycle, no registration step.

The engine resolves tags at render time on the server. If a matching .ree file exists, the tag is replaced with the rendered component output before the response leaves the server. If no matching file exists, the tag passes through unchanged to the browser - where a real JavaScript web component can pick it up. The two forms share the same hyphenated-name convention without ever colliding.

<input-text name="email" label="{_ fields.email.label }" value="{= props.record.email }"></input-text>

That tag resolves to components/input-text.ree. The attributes and slot content are merged on top of the parent's props, so the component can read what you passed and fall back to parent state - props.record, global values - when it needs to.

Invoking a Component

Write the component as a paired ReeTag. The tag name must match components/<name>.ree exactly:

<banner type="green" text="Your changes have been saved."></banner>

The dash is required (that's the web component naming rule) and is also the naming convention for all Ree components - input-text, input-select, banner, pagination.

Passing Data: Attributes and Slots

A component receives two things: attributes (everything on the opening tag, under props.attributes) and children (the slot content between the tags, under props.children).

Attribute values can be static strings or interpolated expressions:

<product-card title="Featured" price="{= product.price_formatted }" on_sale="{= product.discount > 0 }"></product-card>
  • title="Featured" passes the literal string "Featured".
  • price="{= product.price_formatted }" is interpolated - the engine strips the {= } and evaluates product.price_formatted where the tag sits, so the component receives the real value (not the literal text). Use {~ ... } instead of {= ... } for raw, non-stringified values.

Because the tag is expanded in place, an interpolated attribute can read loop variables from a surrounding {#each} - this is how you hand per-item data to a component (see Loops). Slot content, by contrast, is compiled in its own scope and does not see those loop locals.

The slot is everything between the tags:

<card-panel heading="Notes">
    <p>Any markup here is available to the component as {~ props.children }.</p>
</card-panel>

How Props Merge

When the component renders, the engine merges what you passed on top of the parent's props:

Object.assign({}, parent_props, { children, attributes });

That has two consequences worth knowing:

  • The parent's props are still there. If the parent had props.lang, the component still has it - there's no "pass-through" boilerplate to maintain.
  • Your attributes and slot arrive under props.attributes and props.children, keeping per-call data namespaced and separate from the inherited parent state.

Writing a Component

A component is just a .ree file that reads from props.attributes and props.children. Drop a new file into components/ and reference it by its base name - there is no file to register it in; the naming convention is the convention.

A component destructures its attributes (often with a ...rest spread to forward the leftovers) and renders the slot:

{{
const { type, text, ...rest } = props.attributes;
// choose a class based on type …
}}
<div class="{~ final_class } …" ...rest>{~ text }</div>

The ...rest spread shorthand forwards any extra attributes onto the element - it expands to {~ key_values(rest) }.

Components That Ship With ReeWeb

A fresh ReeWeb project includes three starter components in src/components/:

ComponentPurpose
banner.reeStatus/notification banner - renders a styled <div> in green, yellow, red, or neutral based on a type attribute
my-h1.reeHeading component - uppercases its slot content and renders it as an <h1>, demonstrating how to work with props.children and props.attributes
speculation-rules.reeInjects a <script type="speculationrules"> block that enables instant navigation via the browser's Speculation Rules API

Composing Components

Components can include other components - the same merging rules apply at every level. A component reads slot content via props.children and attributes via props.attributes:

<service-item>
    {#each props.services as service}
    <md-text type="h3">{~ service.title }</md-text>
    {/each}
</service-item>

Inside components/service-item.ree, the component accesses the slot via props.children and its attributes via props.attributes.

Components vs Includes

A ReeTag resolves to an include from components/. When the target lives outside components/ - a partial inside a template folder, or a subdirectory like svgs/ - use {#include} directly, which takes an explicit path and an optional data object merged onto the parent props:

{#include('$routes/_partials/breadcrumbs', { trail: props.trail }) }

Use a ReeTag when the target is in components/; use {#include('path', data)} when you need an explicit path elsewhere. Both produce the same kind of merged-props include - the ReeTag is just the ergonomic form for the common, reusable case.