Rewrote all the docs - more noob friendly now.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-05 23:55:26 +05:00
parent e483bf73e7
commit f6ae86617c
35 changed files with 2159 additions and 2341 deletions
+103 -54
View File
@@ -1,16 +1,31 @@
# Creating a New Component
Components are the reusable building blocks of the UI. They follow the same `.htmx` + `.htmx.cs` pair pattern as pages, but they live in `Templates/Components/`, implement `IHtmxComponent`, and are never responsible for HTTP routing.
A component is a **reusable stamp**. You design the stamp once (the `.htmx` template + `.htmx.cs` class), and then press it anywhere you need that piece of UI — on multiple pages, inside other components, even multiple times on the same page.
## The three component patterns
Components are identical in structure to pages, with two key differences:
All existing components fall into one of three shapes. Pick the one that fits what you are building.
1. They live in `Templates/Components/` instead of `Templates/`
2. They are never responsible for HTTP routing — they just render HTML
---
### Pattern A — Simple slot component
## What you want to achieve
Use this when every piece of output is a plain string set from outside.
By the end of this guide you will be able to build any reusable UI piece — a styled label, a card, a form field, or a wrapper that holds other components — and drop it anywhere on a page.
---
## The three patterns
All components fit one of three shapes. Pick the one that matches what you are building.
---
### Pattern A — A simple label or display element
Use this when the component just renders a styled string. It is the simplest case.
**Goal:** a coloured status badge you can reuse in tables, cards, and headers.
```html
<!-- Templates/Components/Badge.htmx -->
@@ -26,13 +41,11 @@ public sealed class Badge : BadgeBase
private readonly byte[] _labelData;
private readonly byte[] _classesData;
// Compute the final class string once in the constructor,
// encode to UTF-8 bytes, never allocate again during render
public Badge(string label, string variant = "default")
{
_labelData = label.ToUtf8Bytes();
var variantClass = variant switch
var variantClasses = variant switch
{
"secondary" => "bg-secondary text-secondary-foreground",
"destructive" => "bg-destructive text-destructive-foreground",
@@ -40,7 +53,7 @@ public sealed class Badge : BadgeBase
_ => "bg-primary text-primary-foreground",
};
_classesData = $"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold {variantClass}"
_classesData = $"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold {variantClasses}"
.ToUtf8Bytes();
}
@@ -49,15 +62,19 @@ public sealed class Badge : BadgeBase
}
```
The key principle: **all computation happens in the constructor**. By the time `RenderLabel` is called during a request, it is just writing pre-computed bytes — no string formatting, no allocations.
---
### Pattern B — Conditionally built sections
### Pattern B — A container with optional sections
Use this when parts of the template are optional (e.g. a card header that only renders when a title is provided). Build the HTML string in the constructor and store as bytes; leave the byte array empty `[]` when not needed — `WriteUtf8` on an empty span is a no-op.
Use this when parts of the component are optional — for example a card that shows a header only when a title is provided.
**Goal:** a card that always shows its body, but optionally shows a header and a footer.
```html
<!-- Templates/Components/Card.htmx -->
<div class="rounded-lg border border-border bg-card text-card-foreground shadow-sm $$ExtraClasses$$">
<div class="rounded-lg border border-border bg-card shadow-sm $$ExtraClasses$$">
$$Header$$
<div class="p-6 pt-0">$$Content$$</div>
$$Footer$$
@@ -85,7 +102,8 @@ public sealed class Card : CardBase
_extraClassesData = extraClasses.ToUtf8Bytes();
_contentData = content.ToUtf8Bytes();
// Header is only rendered when a title or description is supplied
// Build header HTML in the constructor. If there's no title/description,
// store an empty array — writing empty bytes is a no-op.
_headerData = (string.IsNullOrEmpty(title) && string.IsNullOrEmpty(description))
? []
: BuildHeader(title, description);
@@ -116,88 +134,119 @@ public sealed class Card : CardBase
---
### Pattern C — Component slots (embedding other components)
### Pattern C — A wrapper that holds other components
Use this when a slot should itself be rendered by another `IHtmxComponent`. Store the sub-component as a property and call `component.Render(context)` from the override.
Use this when a slot should be filled by another component rather than a string.
**Goal:** a tooltip wrapper — the trigger is any component, and the tooltip text floats above it on hover.
```html
<!-- Templates/Components/MyWrapper.htmx -->
<div class="wrapper p-4">
$$Inner$$
</div>
<!-- Templates/Components/Tooltip.htmx -->
<span class="relative inline-flex items-center group">
$$Trigger$$
<span class="absolute bottom-full mb-1.5 ... opacity-0 group-hover:opacity-100">$$Text$$</span>
</span>
```
```csharp
// Templates/Components/MyWrapper.htmx.cs
// Templates/Components/Tooltip.htmx.cs
namespace Htmx.ApiDemo.Templates.Components;
public sealed class MyWrapper : MyWrapperBase
public sealed class Tooltip : TooltipBase
{
private readonly IHtmxComponent _inner;
private readonly IHtmxComponent _trigger;
private readonly byte[] _textData;
public MyWrapper(IHtmxComponent inner)
public Tooltip(string text, IHtmxComponent trigger)
{
_inner = inner;
_textData = text.ToUtf8Bytes();
_trigger = trigger;
}
// Pass context.Next() so the recursion depth counter increments;
// the runtime throws if nesting exceeds 512 levels
protected override void RenderInner(HtmxRenderContext ctx)
=> _inner.Render(ctx.Next());
protected override void RenderText(HtmxRenderContext ctx)
=> ctx.Writer.WriteUtf8(_textData);
// ctx.Next() increments the nesting depth counter.
// The runtime throws if nesting exceeds 512 levels — this is the guard against infinite loops.
protected override void RenderTrigger(HtmxRenderContext ctx)
=> _trigger.Render(ctx.Next());
}
```
The depth guard (`context.Next()`) is automatically enforced by the infrastructure generated in `HtmxInfrastructure.g.cs`. You do not need to check it yourself.
---
## Embedding a component in a page
## Using a component inside a page
Once a component implements `IHtmxComponent`, use it from a page's code-behind by assigning an instance to an `IHtmxComponent` property and delegating `Render`:
Once you have a component, use it from a page's code-behind. The page stores the component as a field and delegates `Render` from its slot override:
```csharp
// inside MyPage.htmx.cs
public IHtmxComponent MyBadge { get; }
public MyPage(...)
// MyPage.htmx.cs
public sealed class MyPage : MyPageBase
{
MyBadge = new Badge("New", variant: "secondary");
}
private readonly byte[] _headingData;
private readonly IHtmxComponent _statusBadge;
protected override void RenderMyBadge(HtmxRenderContext ctx)
=> MyBadge.Render(ctx.Next());
public MyPage(string heading, string status)
{
_headingData = heading.ToUtf8Bytes();
_statusBadge = new Badge(status, variant: "secondary");
}
protected override void RenderHeading(HtmxRenderContext ctx)
=> ctx.Writer.WriteUtf8(_headingData);
protected override void RenderStatusBadge(HtmxRenderContext ctx)
=> _statusBadge.Render(ctx.Next());
}
```
The corresponding slot in `MyPage.htmx`:
Template:
```html
<div class="flex gap-2">
<span>Status:</span>
$$MyBadge$$
<!-- MyPage.htmx -->
<div class="flex items-center gap-3">
<h1>$$Heading$$</h1>
$$StatusBadge$$
</div>
```
---
## File naming and namespace rules
## A note on HTML safety
| File location | Generated namespace |
`WriteUtf8` writes raw bytes directly to the HTTP response. It does **not** HTML-encode anything.
- Strings you write in the constructor that come from your own code are fine — you control them.
- Any value that comes from user input (a form field, a database value, a query parameter) **must be HTML-encoded before calling `ToUtf8Bytes()`**:
```csharp
// Safe — encodes characters like < > " &
_nameData = System.Web.HttpUtility.HtmlEncode(userInput).ToUtf8Bytes();
```
Skipping this step is a cross-site scripting (XSS) vulnerability.
---
## File location and namespace
| File location | C# namespace |
|---|---|
| `Templates/Components/MyComp.htmx` | `Htmx.ApiDemo.Templates.Components` |
| `Templates/MyPage.htmx` | `Htmx.ApiDemo.Templates` |
The source generator derives the namespace from the folder path relative to the project root. Keep components in `Templates/Components/` so they land in the right namespace and stay separate from page templates.
The source generator derives the namespace from the folder path relative to the project root. Always keep components in `Templates/Components/`.
---
## HTML user content safety
## Checklist
The `WriteUtf8` method writes raw bytes directly to the response. **It does not HTML-encode.**
- Static strings you write in the constructor are trusted — you control them.
- Any value that comes from user input (e.g. a form field, a database string) **must be HTML-encoded before calling `ToUtf8Bytes()`**.
```csharp
- [ ] `.htmx` template created in `Templates/Components/` with `$$PascalCase$$` slots
- [ ] `.htmx.cs` class inherits the generated `XxxBase` class
- [ ] All `RenderXxx` overrides implemented
- [ ] Computation (string building, class selection) done in the constructor
- [ ] User-provided strings HTML-encoded before `ToUtf8Bytes()`
- [ ] Sub-component `Render` calls use `ctx.Next()` not bare `ctx`
// Safe — user-supplied string is encoded first
_displayNameData = System.Web.HttpUtility.HtmlEncode(userDisplayName).ToUtf8Bytes();
```