b530bb8c97
Co-authored-by: Copilot <copilot@github.com>
170 lines
4.5 KiB
Markdown
170 lines
4.5 KiB
Markdown
# Breadcrumb
|
|
|
|
A "you are here" trail — a row of links showing how the user got to the current page. Like breadcrumbs leading back through a forest.
|
|
|
|
---
|
|
|
|
## Quick example
|
|
|
|
```csharp
|
|
new Breadcrumb(new[]
|
|
{
|
|
("Home", "/"),
|
|
("Settings", "/settings"),
|
|
("Profile", ""), // current page
|
|
})
|
|
```
|
|
|
|
The last item is always the current page. Its link is ignored — the component automatically renders it as plain text with full colour instead of a dimmed link.
|
|
|
|
---
|
|
|
|
## All the options
|
|
|
|
```csharp
|
|
public Breadcrumb(IEnumerable<(string Label, string Href)> items)
|
|
```
|
|
|
|
| Parameter | What it does |
|
|
|---|---|
|
|
| `items` | An ordered list of `(Label, Href)` pairs from root to current page. |
|
|
|
|
Two rules:
|
|
- The **last item** is always rendered as plain text (current page). Its `Href` is ignored.
|
|
- Any **non-last item** with an empty `Href` renders as a plain `<span>` — useful for non-navigable category labels.
|
|
|
|
---
|
|
|
|
## Real-world examples
|
|
|
|
### Three-level app navigation
|
|
|
|
```csharp
|
|
new Breadcrumb(new[]
|
|
{
|
|
("Home", "/"),
|
|
("Reports", "/reports"),
|
|
("Monthly", ""), // current — href not needed
|
|
})
|
|
```
|
|
|
|
### Built dynamically from a category tree
|
|
|
|
```csharp
|
|
var crumbs = categoryPath
|
|
.Select((cat, i) => (cat.Name, i < categoryPath.Count - 1 ? cat.Url : ""))
|
|
.ToArray();
|
|
|
|
new Breadcrumb(crumbs)
|
|
```
|
|
|
|
### Inside a page
|
|
|
|
```html
|
|
<!-- ArticlePage.htmx -->
|
|
<div class="mb-6">$$Breadcrumb$$</div>
|
|
<h1 class="text-3xl font-bold">$$ArticleTitle$$</h1>
|
|
```
|
|
|
|
```csharp
|
|
// ArticlePage.htmx.cs
|
|
public sealed class ArticlePage : ArticlePageBase
|
|
{
|
|
private readonly IHtmxComponent _breadcrumb;
|
|
private readonly byte[] _titleData;
|
|
|
|
public ArticlePage(string articleTitle, string categoryName, string categoryUrl)
|
|
{
|
|
_titleData = articleTitle.ToUtf8Bytes();
|
|
_breadcrumb = new Breadcrumb(new[]
|
|
{
|
|
("Home", "/"),
|
|
(categoryName, categoryUrl),
|
|
(articleTitle, ""),
|
|
});
|
|
}
|
|
|
|
protected override void RenderBreadcrumb(HtmxRenderContext ctx)
|
|
=> _breadcrumb.Render(ctx.Next());
|
|
|
|
protected override void RenderArticleTitle(HtmxRenderContext ctx)
|
|
=> ctx.Writer.WriteUtf8(_titleData);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## How it works
|
|
|
|
Each item renders as a `<li>` inside an `<ol>` inside a `<nav aria-label="Breadcrumb">`. All items except the last are rendered as `<a>` links; the last is a `<span>`. Between items the component inserts a small SVG chevron that is marked `aria-hidden` so screen readers skip it and only announce the text labels.
|
|
|
|
---
|
|
|
|
## Complete page example
|
|
|
|
**`Templates/ArticlePage.htmx`**
|
|
```html
|
|
<div class="max-w-3xl mx-auto py-10">
|
|
<div class="mb-6">$$Breadcrumb$$</div>
|
|
<h1 class="text-3xl font-bold mb-4">$$ArticleTitle$$</h1>
|
|
<div class="prose">$$ArticleBody$$</div>
|
|
</div>
|
|
```
|
|
|
|
**`Templates/ArticlePage.htmx.cs`**
|
|
```csharp
|
|
namespace Htmx.ApiDemo.Templates;
|
|
|
|
public sealed class ArticlePage : ArticlePageBase
|
|
{
|
|
private readonly IHtmxComponent _breadcrumb;
|
|
private readonly byte[] _title;
|
|
private readonly byte[] _body;
|
|
|
|
public ArticlePage(string category, string categorySlug, Article article)
|
|
{
|
|
_breadcrumb = new Components.Breadcrumb(new[]
|
|
{
|
|
("Home", "/"),
|
|
("Blog", "/blog"),
|
|
(category, $"/blog/{categorySlug}"),
|
|
(article.Title, ""), // current page
|
|
});
|
|
|
|
_title = System.Net.WebUtility.HtmlEncode(article.Title).ToUtf8Bytes();
|
|
_body = article.HtmlContent.ToUtf8Bytes();
|
|
}
|
|
|
|
protected override void RenderBreadcrumb(HtmxRenderContext ctx)
|
|
=> _breadcrumb.Render(ctx.Next());
|
|
protected override void RenderArticleTitle(HtmxRenderContext ctx)
|
|
=> ctx.Writer.WriteUtf8(_title);
|
|
protected override void RenderArticleBody(HtmxRenderContext ctx)
|
|
=> ctx.Writer.WriteUtf8(_body);
|
|
}
|
|
```
|
|
|
|
**GET handler**
|
|
```csharp
|
|
[Handler]
|
|
[MapGet("/blog/{category}/{slug}")]
|
|
public static partial class GetArticleHandler
|
|
{
|
|
public record Query(
|
|
[property: FromRoute] string Category,
|
|
[property: FromRoute] string Slug);
|
|
|
|
private static async Task<IResult> HandleAsync(
|
|
Query q,
|
|
HttpContext ctx,
|
|
ArticleService articles,
|
|
CancellationToken ct)
|
|
{
|
|
var article = await articles.GetBySlugAsync(q.Slug, ct);
|
|
if (article is null) return Results.NotFound();
|
|
var page = new ArticlePage(q.Category, q.Category.ToLower(), article);
|
|
return await ctx.WriteHtmxPage(page, title: article.Title);
|
|
}
|
|
}
|
|
```
|