b530bb8c97
Co-authored-by: Copilot <copilot@github.com>
200 lines
6.2 KiB
Markdown
200 lines
6.2 KiB
Markdown
# Creating a New Page
|
|
|
|
Think of a page as a **form letter** — the template is the letter with blanks left for personalisation, and your C# class is the person who fills those blanks in before the letter is sent. The build system generates all the plumbing between the two; you just write the template and the class.
|
|
|
|
---
|
|
|
|
## What you want to achieve
|
|
|
|
By the end of this guide you will have a new page at a URL like `/dashboard` that:
|
|
|
|
- Shows your own custom HTML
|
|
- Loads instantly as a full page when you visit the URL directly
|
|
- Swaps in as a smooth partial update when navigated to from the sidebar
|
|
- Accepts data you pass to it from C#
|
|
|
|
---
|
|
|
|
## The two files every page needs
|
|
|
|
| File | What it is |
|
|
|---|---|
|
|
| `Templates/MyPage.htmx` | The letter template — HTML with `$$Slot$$` blanks |
|
|
| `Templates/MyPage.htmx.cs` | The person filling in the blanks — C# class + route handler |
|
|
|
|
The build system (`Htmx.SourceGenerator`) reads your `.htmx` file and generates an abstract C# class with one `RenderXxx()` method per `$$Slot$$`. Your job is to inherit that class and implement each method.
|
|
|
|
---
|
|
|
|
## Step 1 — Write the template
|
|
|
|
Create `Htmx.ApiDemo/Templates/MyPage.htmx`:
|
|
|
|
```html
|
|
<div class="p-6 space-y-4">
|
|
<h1 class="text-2xl font-bold text-foreground">$$Heading$$</h1>
|
|
<p class="text-muted-foreground">$$Description$$</p>
|
|
</div>
|
|
```
|
|
|
|
Rules for slots:
|
|
- Names are **PascalCase** surrounded by `$$` — e.g. `$$MySlot$$`
|
|
- A slot can contain plain text, HTML, or a rendered component
|
|
- The file must live inside `Templates/` so the build picks it up automatically
|
|
|
|
After saving this file and building, the generator emits `MyPageBase` — a class you will never edit but will inherit from.
|
|
|
|
---
|
|
|
|
## Step 2 — Write the code-behind
|
|
|
|
Create `Htmx.ApiDemo/Templates/MyPage.htmx.cs`:
|
|
|
|
```csharp
|
|
namespace Htmx.ApiDemo.Templates;
|
|
|
|
public sealed class MyPage : MyPageBase
|
|
{
|
|
private readonly byte[] _headingData;
|
|
private readonly byte[] _descriptionData;
|
|
|
|
public MyPage(string heading, string description)
|
|
{
|
|
// Convert strings to UTF-8 bytes once in the constructor.
|
|
// The Render methods then just write those bytes — no allocations at request time.
|
|
_headingData = heading.ToUtf8Bytes();
|
|
_descriptionData = description.ToUtf8Bytes();
|
|
}
|
|
|
|
protected override void RenderHeading(HtmxRenderContext ctx)
|
|
=> ctx.Writer.WriteUtf8(_headingData);
|
|
|
|
protected override void RenderDescription(HtmxRenderContext ctx)
|
|
=> ctx.Writer.WriteUtf8(_descriptionData);
|
|
}
|
|
```
|
|
|
|
The pattern here is deliberate: do all string work (formatting, encoding) in the constructor, so that `Render` is nothing but memory writes. This keeps request handling fast.
|
|
|
|
---
|
|
|
|
## Step 3 — Write the route handler
|
|
|
|
Route handlers live in the same `.htmx.cs` file. They are plain static methods registered with Minimal API — no special framework, no base class, no attributes from removed packages:
|
|
|
|
```csharp
|
|
namespace Htmx.ApiDemo.Templates;
|
|
|
|
public static class MyPageEndpoints
|
|
{
|
|
public static void Map(IEndpointRouteBuilder app)
|
|
{
|
|
app.MapGet("/my-page", Handle);
|
|
}
|
|
|
|
private static IResult Handle(HttpContext ctx)
|
|
{
|
|
var page = new MyPage(
|
|
heading: "My New Page",
|
|
description: "This is a minimal example."
|
|
);
|
|
|
|
ctx.WriteHtmxPage(page, title: "My Page");
|
|
return Results.Empty;
|
|
}
|
|
}
|
|
```
|
|
|
|
Then register it in `Program.cs` alongside the other endpoint registrations:
|
|
|
|
```csharp
|
|
MyPageEndpoints.Map(app);
|
|
```
|
|
|
|
---
|
|
|
|
## Step 4 — Add a sidebar link (optional but typical)
|
|
|
|
Open `Templates/MainLayout.htmx` and add a nav entry inside the `<nav>` block:
|
|
|
|
```html
|
|
<a href="/my-page"
|
|
hx-get="/my-page" hx-target="#main-view" hx-push-url="true"
|
|
class="sidebar-link group flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium
|
|
text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground">
|
|
My Page
|
|
</a>
|
|
```
|
|
|
|
The three HTMX attributes do the heavy lifting:
|
|
|
|
| Attribute | What it does |
|
|
|---|---|
|
|
| `hx-get="/my-page"` | Fetches the page as a partial instead of a full reload |
|
|
| `hx-target="#main-view"` | Drops the response into the content area, leaving the sidebar untouched |
|
|
| `hx-push-url="true"` | Updates the browser URL bar so bookmarks and back-button still work |
|
|
|
|
---
|
|
|
|
## How the app knows whether to send a full page or just the fragment
|
|
|
|
When `WriteHtmxPage` is called it checks for the `HX-Request` header that HTMX sends on every HTMX-triggered request:
|
|
|
|
```
|
|
Direct browser visit (no HX-Request header)
|
|
→ full HTML: <html><head>...</head><body><sidebar/><main>YOUR PAGE</main></body></html>
|
|
|
|
HTMX sidebar click (HX-Request: true)
|
|
→ just your fragment: <div class="p-6 space-y-4">...</div>
|
|
→ plus an HX-Title header so the browser tab title still updates
|
|
```
|
|
|
|
You never need to branch on this yourself. `WriteHtmxPage` handles it.
|
|
|
|
---
|
|
|
|
## Embedding a component inside a page
|
|
|
|
Slots are not limited to text. If you want to place a reusable component inside a slot, store it as a field and call `Render` from the override:
|
|
|
|
```csharp
|
|
public sealed class MyPage : MyPageBase
|
|
{
|
|
private readonly byte[] _headingData;
|
|
private readonly IHtmxComponent _statusBadge;
|
|
|
|
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()); // ctx.Next() tracks nesting depth
|
|
}
|
|
```
|
|
|
|
In the template:
|
|
|
|
```html
|
|
<div class="flex items-center gap-3">
|
|
<h1>$$Heading$$</h1>
|
|
$$StatusBadge$$
|
|
</div>
|
|
```
|
|
|
|
See [03-creating-a-component.md](03-creating-a-component.md) for how to build your own components.
|
|
|
|
---
|
|
|
|
## Checklist
|
|
|
|
- [ ] `MyPage.htmx` saved in `Templates/` with `$$PascalCase$$` slots
|
|
- [ ] `MyPage.htmx.cs` has a class inheriting `MyPageBase` with all `RenderXxx` overrides
|
|
- [ ] Route handler registered in `Program.cs`
|
|
- [ ] Builds cleanly — the compiler will error if any slot override is missing
|
|
- [ ] Sidebar link added to `MainLayout.htmx` if the page needs to be in the nav
|