Creating a Component
Every Tesserae component is a C# class that produces a DOM element. The contract
is a single interface, IComponent:
namespace Tesserae
{
public interface IComponent
{
HTMLElement Render();
}
}
A parent container (or one of the mount helpers) calls Render() to splice your
element into the page. The usual approach is to build the DOM once — in the
constructor — keep the root element in a field, and return it from Render().
There are two starting points depending on how much behaviour you need.
Option 1 — implement IComponent directly
Use this for display-only widgets that don't need click/focus/change events. You
build the element tree with the DOM helpers from Tesserae.UI and return it.
using static H5.Core.dom;
using static Tesserae.UI; // Div / Span / I and the _( ) attributes helper
namespace Tesserae
{
[H5.Name("tss.MyBadge")]
public class MyBadge : IComponent
{
private readonly HTMLElement _inner;
public MyBadge(string text)
{
_inner = Div(_("tss-mybadge"), Span(_(text: text)));
}
public HTMLElement Render() => _inner;
}
}
_( ) builds an Attributes object — _("css-class"), _(text: "hello"),
_(id: "x", styles: s => s.color = "red"). The DOM builders (Div, Span,
I, DIV(), Raw(html), …) live in UI.HtmlUtil. The [H5.Name("tss.…")]
attribute names the generated JavaScript class; it is optional but conventional
across the toolkit.
Option 2 — derive from ComponentBase<T, THTML>
Use this when you want events, fluent configuration, margin/padding, and ARIA
support out of the box. ComponentBase provides InnerElement, the
OnClick/OnChange/OnFocus family, Margin/Padding, and
AriaLabel/AriaRole. Set InnerElement in the constructor and return it from
Render().
using static H5.Core.dom;
using static Tesserae.UI;
namespace Tesserae
{
[H5.Name("tss.MyToggle")]
public class MyToggle : ComponentBase<MyToggle, HTMLDivElement>
{
public MyToggle()
{
InnerElement = Div(_("tss-mytoggle"));
AttachClick(); // wire up the base class's click plumbing
}
public override HTMLElement Render() => InnerElement;
// Fluent helpers return `this` so calls can be chained.
public MyToggle On()
{
InnerElement.classList.add("tss-on");
return this;
}
}
}
The generic parameters are the component's own type (so fluent methods return the concrete type) and the backing HTML element type.
Sizing, alignment, and containers
The sizing helpers — .Width(), .WS(), .Grow(), .Stretch(), and friends —
work on any IComponent. They tag the element with marker attributes; when a
container wraps your component in its item <div>, it transfers those styles
onto the wrapper (the "wrap-and-transfer" protocol). You don't need to do
anything special to support them.
If your component is itself a container that shouldn't be wrapped (for example a
Grid nested in a Stack), implement ISpecialCaseStyling and expose a
StylingContainer so the helpers write directly onto it.
To accept children, implement IContainer<T, TChild>. In practice most custom
components compose existing ones instead — returning a Stack().Children(...)
from Render() is often simpler than managing children yourself.
Registering the component
Once the class exists, wire it into the toolkit the same way the built-in components are:
Put the class under
Tesserae/src/Components/.Add a factory method in
Tesserae/src/Base/UI.Components.cs:public static MyBadge MyBadge(string text) => new MyBadge(text);Add fluent helpers or extension methods under
Tesserae/src/Extensions/if the component needs them.Add a sample under
Tesserae.Tests/demonstrating it.
Mounting
A top-level component is attached to the page with MountToBody(component) or
MountCenteredToBody(component):
private static void Main()
{
document.body.style.overflow = "hidden";
MountCenteredToBody(new MyBadge("Ready"));
}
See also
- JavaScript Interop — reach browser
APIs from inside
Render(). - Wrapping a JavaScript Library — back a component with a third-party JS library.
- Core Concepts — how composition and rendering fit together.