From b530bb8c979740f91c17c35b0655413a98be91e3 Mon Sep 17 00:00:00 2001 From: Enciphered Date: Tue, 5 May 2026 23:55:26 +0500 Subject: [PATCH] Rewrote all the docs - more noob friendly now. Co-authored-by: Copilot --- docs/01-getting-started.md | 196 +++++++++++++++---- docs/02-creating-a-page.md | 254 +++++++++++++----------- docs/03-creating-a-component.md | 157 +++++++++------ docs/04-data-models-and-aot.md | 149 +++++++++----- docs/05-form-submission.md | 323 +++++++++++++++++-------------- docs/06-component-reference.md | 115 ++++++++--- docs/Components/Accordion.md | 198 +++++++++---------- docs/Components/Alert.md | 100 ++++------ docs/Components/Avatar.md | 87 ++++----- docs/Components/Badge.md | 118 ++++++----- docs/Components/Breadcrumb.md | 134 ++++++------- docs/Components/Button.md | 144 +++++++------- docs/Components/Calendar.md | 152 ++++++--------- docs/Components/CalendarRange.md | 142 +++++--------- docs/Components/Card.md | 122 ++++++------ docs/Components/Checkbox.md | 134 ++++++------- docs/Components/Dialog.md | 143 +++++--------- docs/Components/DropdownMenu.md | 141 ++++++-------- docs/Components/FileInput.md | 115 +++++------ docs/Components/Input.md | 91 ++++----- docs/Components/Pagination.md | 97 ++++------ docs/Components/Progress.md | 81 +++----- docs/Components/RadioGroup.md | 105 +++++----- docs/Components/Select.md | 128 ++++++------ docs/Components/Separator.md | 68 +++---- docs/Components/Skeleton.md | 104 +++++----- docs/Components/Slider.md | 98 +++++----- docs/Components/Switch.md | 96 ++++----- docs/Components/Table.md | 99 ++++------ docs/Components/Tabs.md | 125 ++++++------ docs/Components/Textarea.md | 90 ++++----- docs/Components/TimePicker.md | 126 +++++------- docs/Components/Toast.md | 111 ++++------- docs/Components/ToastViewport.md | 72 ++----- docs/Components/Tooltip.md | 85 ++++---- 35 files changed, 2159 insertions(+), 2341 deletions(-) diff --git a/docs/01-getting-started.md b/docs/01-getting-started.md index 48550ed..df75fc1 100644 --- a/docs/01-getting-started.md +++ b/docs/01-getting-started.md @@ -1,32 +1,141 @@ # Getting Started -This guide gets the solution running locally and explains what happens during startup. +This guide walks you through everything you need to get the project running locally — from installing tools to understanding why certain architectural decisions were made. + +--- ## What is in this solution -- `Htmx.ApiDemo`: ASP.NET Core app (Minimal API + generated HTMX endpoints) -- `Htmx.SourceGenerator`: Roslyn source generator that discovers `.htmx` files and generates endpoint mapping code -- `Htmx.slnx`: solution file at the repository root +| Project | Purpose | +|---|---| +| `Htmx.ApiDemo` | ASP.NET Core web app using Minimal APIs and server-rendered HTMX templates | +| `Htmx.SourceGenerator` | Roslyn source generator that reads `.htmx` template files and generates endpoint mapping code at build time | + +The solution file is `Htmx.slnx` at the repository root. + +--- ## Prerequisites -- .NET SDK 10.x (target framework is `net10.0`) -- Node.js + npm (used for Tailwind CSS compilation during build) -- MongoDB running locally on `mongodb://localhost:27017` +Install the following before cloning the repo. + +### .NET SDK + +The project targets `net10.0`. Download the .NET 10 SDK from [dot.net](https://dotnet.microsoft.com/download). + +Verify with: + +```bash +dotnet --version +``` + +### Node.js and npm + +Tailwind CSS is compiled during the build using the Tailwind CLI via `npx`. Node.js must be installed. + +Download from [nodejs.org](https://nodejs.org). Verify with: + +```bash +node -v +npm -v +``` + +### MongoDB + +The app stores data in MongoDB. You need a local instance running on `mongodb://localhost:27017`. + +**Windows:** + +Download and install [MongoDB Community Server](https://www.mongodb.com/try/download/community). During installation, choose to run MongoDB as a Windows Service so it starts automatically. + +**Linux:** + +Follow the official guide for your distro at [docs.mongodb.com/manual/administration/install-on-linux](https://www.mongodb.com/docs/manual/administration/install-on-linux/). For Ubuntu/Debian: + +```bash +sudo systemctl start mongod +sudo systemctl enable mongod # start on boot +``` + +**MongoDB Compass (optional but recommended):** + +Compass is a GUI for browsing and querying your MongoDB data. Download it from [mongodb.com/products/compass](https://www.mongodb.com/products/compass). Connect it to `mongodb://localhost:27017` to inspect collections while developing. + +--- + +## VS Code setup + +### Required extensions + +- **C# Dev Kit** — provides IntelliSense, debugging, and project support for .NET + Search for `ms-dotnettools.csdevkit` in the Extensions panel. + +- **C# (OmniSharp / Roslyn)** — included with C# Dev Kit but can also be installed standalone as `ms-dotnettools.csharp`. + +### Recommended settings + +Add the following to your workspace or user `settings.json`. This teaches VS Code to treat `.htmx` files as HTML (for syntax highlighting and formatting) and nests generated companion files under their parent in the Explorer sidebar so the file tree stays clean. + +```jsonc +{ + "files.associations": { + "*.htmx": "html" + }, + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.expand": false, + "explorer.fileNesting.patterns": { + "*.razor": "$(capture).razor.cs, $(capture).razor.css, $(capture).razor.js", + "*.htmx": "${capture}.htmx.cs, ${capture}.htmx.routing.cs, ${capture}.g.cs, ${capture}.css" + } +} +``` + +Without `files.associations`, `.htmx` files open as plain text with no highlighting. Without file nesting, every generated `.htmx.cs` and `.htmx.routing.cs` file appears as a separate top-level entry in the Explorer, making it hard to navigate. + +### Optional extension + +- **Tailwind CSS Fold** — collapses long Tailwind class strings in the editor so markup is easier to read. Search for `stivo.tailwind-fold`. This is purely a cosmetic convenience and has no effect on the build. + +--- + +## Understanding AOT + +AOT (Ahead-of-Time compilation) means the app is compiled to native machine code before it runs, rather than relying on the .NET JIT at runtime. This project has AOT enabled (`true` in the `.csproj`). + +### Why AOT matters here + +AOT produces smaller, faster deployments with no JIT warmup time. For a web app handling many requests, startup time and binary size are real concerns — especially in containerized or serverless environments. + +### What AOT prevents you from doing + +AOT is a significant constraint. It eliminates entire categories of patterns that are common in standard .NET development: + +- **No Entity Framework Core** — EF Core relies heavily on runtime reflection and expression compilation. It is not AOT-compatible. This project uses the MongoDB driver directly instead. + +- **No runtime reflection** — `Type.GetProperties()`, `Activator.CreateInstance()`, dynamic proxies, and similar patterns do not work (or produce warnings/errors) under AOT. If a pattern depends on inspecting types at runtime, it will not survive. + +- **Many NuGet packages are incompatible** — Any package that uses reflection internally (serializers, mappers, validators, ORMs, DI containers with convention scanning, etc.) may break. Check a package's AOT compatibility before adding it. + +- **Source generator-based serialization required** — Rather than `JsonSerializer.Serialize(myObject)` discovering properties at runtime, you must register types with a `JsonSerializerContext` subclass (see `AppJsonSerializerContext.cs`). The serializer then uses generated code instead of reflection. + +- **Route handler code generation** — ASP.NET Core's Minimal API generator produces code for request binding and response writing. Some third-party packages produce code that conflicts with this. If adding a package causes build errors in generated files, AOT incompatibility is the likely cause. + +The practical rule: before reaching for a package or pattern you know from standard ASP.NET Core, check whether it is AOT-compatible. The project will compile normally in Debug mode even with AOT-incompatible code — AOT issues typically only surface during `dotnet publish`. + +--- ## First-time setup -From the repository root: +Clone the repo, then install the npm dependencies that the build needs: ```bash cd Htmx.ApiDemo npm install ``` -Why this is required: +This installs the Tailwind CSS CLI package. The build runs `npx @tailwindcss/cli` as an MSBuild step, so if this is skipped the build will fail with a missing command error. -- The app build runs Tailwind via `npx @tailwindcss/cli ...` in a custom MSBuild target. -- Without `npm install`, build fails because the Tailwind CLI package is missing. +--- ## Run the app @@ -36,57 +145,62 @@ From the repository root: dotnet run --project Htmx.ApiDemo/Htmx.ApiDemo.csproj ``` -Default local URL: +The app listens on `http://localhost:5120` by default (configured in `Htmx.ApiDemo/Properties/launchSettings.json`). -- `http://localhost:5120` - -This comes from the launch profile in `Htmx.ApiDemo/Properties/launchSettings.json`. +--- ## Verify it works -1. Open `http://localhost:5120` -2. If you are not authenticated, middleware redirects to `/login` -3. Create an account at `/register` -4. Sign in and navigate the app +1. Open `http://localhost:5120` in your browser. +2. If you are not signed in, the middleware redirects you to `/login` — this is expected. +3. Go to `/register` and create an account. +4. Sign in and explore the app. -## What startup config does +--- -`Htmx.ApiDemo/Program.cs` configures: +## What happens at startup -- MongoDB DI and index initialization (`EnsureIndexesAsync`) -- Cookie authentication + authorization +`Program.cs` wires up the following in order: + +- MongoDB service registration and index initialization (`EnsureIndexesAsync`) +- Cookie-based authentication and authorization - Antiforgery middleware -- AOT-friendly JSON resolver chain using `AppJsonSerializerContext` -- Endpoint registration via generated mapping call: - - `app.MapHtmxApiDemoEndpoints();` +- AOT-compatible JSON serialization via `AppJsonSerializerContext` +- All generated HTMX endpoints via `app.MapHtmxApiDemoEndpoints()` -## Build behavior worth knowing +--- -- Tailwind CSS is compiled before build into `Htmx.ApiDemo/wwwroot/css/output.css` -- `.htmx` files are treated as generator inputs (``) -- AOT is enabled (`true`), so reflection-heavy patterns can break publish/runtime +## Build details -## Optional: publish as AOT +- **Tailwind** is compiled into `wwwroot/css/output.css` as a pre-build step. +- **`.htmx` files** are passed to the source generator as ``. The generator reads them and produces the abstract base classes and routing code. +- **AOT** is active on publish. Run a publish build early and often to catch incompatibilities before they accumulate. + +### Publish (AOT) ```bash dotnet publish Htmx.ApiDemo/Htmx.ApiDemo.csproj -c Release ``` -Use this early to catch AOT issues while developing features. +--- ## Troubleshooting -### Build fails on Tailwind command +### Build fails — Tailwind command not found -- Run `npm install` inside `Htmx.ApiDemo` -- Confirm `node -v` and `npm -v` are available +Run `npm install` inside the `Htmx.ApiDemo` directory. Confirm `node` and `npm` are on your `PATH`. -### Mongo connection errors +### MongoDB connection errors at startup -- Confirm MongoDB is running on `localhost:27017` -- Confirm `ConnectionStrings:DefaultConnection` in `Htmx.ApiDemo/appsettings.json` +- Confirm the MongoDB service is running (`mongod`). +- Check that `ConnectionStrings:DefaultConnection` in `appsettings.json` points to `mongodb://localhost:27017`. -### App keeps redirecting to login +### App always redirects to `/login` -- This is expected for unauthenticated routes -- Register at `/register` or sign in at `/login` +This is intentional. Unauthenticated requests are redirected by the auth middleware. Register at `/register` first. + +### AOT warnings or errors on publish + +- Look at the warning message — it usually names the type or method causing the issue. +- Remove or replace the offending package or pattern with an AOT-compatible alternative. +- Run `dotnet publish` regularly during development so issues do not pile up. diff --git a/docs/02-creating-a-page.md b/docs/02-creating-a-page.md index 29f1ea4..b93879b 100644 --- a/docs/02-creating-a-page.md +++ b/docs/02-creating-a-page.md @@ -1,58 +1,32 @@ # Creating a New Page -This guide explains the full lifecycle of adding a new page to the app: the template file, the code-behind class, the handler, and the sidebar link. +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. -## How pages work +--- -Every page is a pair of files: +## What you want to achieve -| File | Purpose | +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` | HTML markup with `$$SlotName$$` slots | -| `Templates/MyPage.htmx.cs` | C# class that fills slots + declares the Minimal API handler | +| `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 Roslyn source generator (`Htmx.SourceGenerator`) reads every `.htmx` file at build time and generates an abstract base class for it. You then write a concrete class in the companion `.htmx.cs` file that inherits from that base. +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. -## How `$$SlotName$$` becomes code +--- -Take this simple template: - -```html - -
-

$$Title$$

-

$$Body$$

-
-``` - -The generator splits the file on `$$...$$` patterns and produces: - -```csharp -// auto-generated — do NOT edit -public abstract partial class MyPageBase : IHtmxComponent -{ - protected abstract void RenderTitle(HtmxRenderContext context); - protected abstract void RenderBody(HtmxRenderContext context); - - // static HTML segments stored as ReadOnlySpan for zero-allocation output - private static ReadOnlySpan _part0 => new byte[] { ... }; - private static ReadOnlySpan _part1 => new byte[] { ... }; - private static ReadOnlySpan _part2 => new byte[] { ... }; - - public void Render(HtmxRenderContext context) - { - context.Writer.WriteUtf8(_part0); //

- RenderTitle(context.Next()); - context.Writer.WriteUtf8(_part1); //

- RenderBody(context.Next()); - context.Writer.WriteUtf8(_part2); //

- } -} -``` - -Your job is to write the concrete class that implements each `RenderXxx` method. - -## Step 1 — Create the `.htmx` template +## Step 1 — Write the template Create `Htmx.ApiDemo/Templates/MyPage.htmx`: @@ -63,119 +37,163 @@ Create `Htmx.ApiDemo/Templates/MyPage.htmx`: ``` -Rules: -- Slot names are **PascalCase** and surrounded by `$$` — e.g. `$$MySlot$$` -- A slot can hold plain text, HTML, or another rendered component -- The file must be saved in `Templates/` (or a subfolder) so the `.csproj` `AdditionalFiles` glob picks it up +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 -## Step 2 — Create the `.htmx.cs` code-behind +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 -using Immediate.Apis.Shared; -using Immediate.Handlers.Shared; - namespace Htmx.ApiDemo.Templates; -// Concrete template — inherits from the generated base public sealed class MyPage : MyPageBase { - private byte[] _headingData = []; - private byte[] _descriptionData = []; + private readonly byte[] _headingData; + private readonly byte[] _descriptionData; - // Use `init`-only setters to pre-encode strings to UTF-8 bytes once - public required string Heading { init => _headingData = value.ToUtf8Bytes(); } - public required string Description { init => _descriptionData = value.ToUtf8Bytes(); } - - protected override void RenderHeading(HtmxRenderContext context) - => context.Writer.WriteUtf8(_headingData); - - protected override void RenderDescription(HtmxRenderContext context) - => context.Writer.WriteUtf8(_descriptionData); -} - -// Minimal API handler — discovered and registered by the source generator -[Handler] -[MapGet("/my-page")] -public static partial class GetMyPageHandler -{ - public record Query; // add route/query parameters here if needed - - private static ValueTask HandleAsync( - Query query, - IHttpContextAccessor httpContextAccessor, - CancellationToken token) + public MyPage(string heading, string description) { - var ctx = httpContextAccessor.HttpContext - ?? throw new InvalidOperationException("HttpContext is not available."); + // 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(); + } - var page = new MyPage - { - Heading = "My New Page", - Description = "This is a minimal example page." - }; + protected override void RenderHeading(HtmxRenderContext ctx) + => ctx.Writer.WriteUtf8(_headingData); - // WriteHtmxPage: full HTML shell for direct browser loads, - // bare fragment for HTMX partial swaps (HX-Request header present) - ctx.WriteHtmxPage(page, title: "My Page", appName: "HtmxApp", pageTitle: "My Page"); - return ValueTask.CompletedTask; + 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; } } ``` -## Step 3 — Add a sidebar link (optional but typical) +Then register it in `Program.cs` alongside the other endpoint registrations: -Open `Templates/MainLayout.htmx` and add a nav entry inside the `