Skip to content

Commit

Permalink
fix: document standard and direct routing, introduce RoutingMode enum
Browse files Browse the repository at this point in the history
  • Loading branch information
egil committed May 4, 2024
1 parent 957a48d commit 765d4a4
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 12 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,9 @@ page title during htmx requests by using the <PageTitle></PageTitle> component.
+ @attribute [HxLayout(typeof(HtmxorLayout))]
```

Note that we set up the custom layout for all components by defining the `[HxLayout(typeof(HtmxorLayout))]` attribute in the `_Imports.razor` file.
Note that we set up the custom layout for all components by defining the `[HxLayout(typeof(HtmxorLayout))]` attribute in the `_Imports.razor` file.

## Documentation

- **[Routing in Htmxor](/docs/routing.md)**: Covers how routing works in Htmor, compared to Blazor Static Web apps.

50 changes: 50 additions & 0 deletions docs/routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Routing in Htmxor

Htmxor routing and Blazor Static Web Apps routing differ in ways that enhance htmx scenarios. In Htmxor, there are two types of routing:

In Htmxor, there are **two** types of routing:

- **Standard routing**
- **Direct routing**

The routing mode is determined by the presence or absence of [htmx headers](https://htmx.org/reference/#request_headers):

```
if ( HX-Request is null || ( HX-Boosted is not null && HX-Target is null ) )
RoutingMode.Standard
else
RoutingMode.Direct
```

Here's a detailed look at each mode:

## Standard Routing

Standard routing is used when the `HX-Request` header is missing, or when `HX-Boosted` is present and `HX-Target` is missing.

In this mode, routing behaves like conventional Blazor Static Web Apps routing. The root component (typically App.razor or the component passed to `MapRazorComponents<TRootComponent>()` in `Program.cs`) is rendered.

The root component usually renders a `<Router>` component that determines which `@page`-annotated component to render based on the HTTP request, using the layout specified for that page.

Example:

```
HTTP GET /my-page
App -> Routes --> MainLayout --> MyPage
```

## Direct Routing:

Direct routing bypasses the root component (`App.razor`) and the standard layout (`MainLayout`). Instead, it routes directly to the component that matches the request.

If the target component has a `HtmxLayout` attribute, that layout is rendered first.

Example:

```
HTTP GET /my-htmx-page
HtmxLayout --> MyHtmxPage
MyHtmxPage
```

This allows `MyHtmxPage` to be rendered directly, optionally including a specified `HtmxLayout`.
3 changes: 2 additions & 1 deletion src/Htmxor/Builder/ComponentEndpointMatcherPolicy.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Htmxor.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
Expand Down Expand Up @@ -33,7 +34,7 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
var endpoint = candidates[i].Endpoint;
var htmxorEndpointMetadata = endpoint.Metadata.GetMetadata<EndpointMetadata>();

if (htmxorEndpointMetadata is null && !htmxContext.Request.IsFullPageRequest)
if (htmxorEndpointMetadata is null && htmxContext.Request.RoutingMode is RoutingMode.Direct)
{
candidates.SetValidity(i, false);
continue;
Expand Down
2 changes: 1 addition & 1 deletion src/Htmxor/Components/ConditionalComponentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ public abstract class ConditionalComponentBase : ComponentBase, IConditionalOutp
/// <remarks>The <see cref="ConditionalComponentBase"/> defaults to returning <see langword="true"/>
/// when the request is a full page request or if there are no direct conditional children.</remarks>
public virtual bool ShouldOutput([NotNull] HtmxContext context, int directConditionalChildren, int conditionalChildren)
=> context.Request.IsFullPageRequest || directConditionalChildren == 0;
=> context.Request.RoutingMode is RoutingMode.Standard || directConditionalChildren == 0;
}
16 changes: 9 additions & 7 deletions src/Htmxor/Http/HtmxRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@ namespace Htmxor.Http;
public sealed class HtmxRequest
{
/// <summary>
/// Gets whether or not the current request will be treated as a full page request.
/// Gets the routing mode for the current request.
/// </summary>
/// <remarks>
/// The rules are as follows: <c>!IsHtmxRequest || (IsBoosted &amp;&amp; Target is null)</c>
/// </remarks>
public bool IsFullPageRequest => !IsHtmxRequest || (IsBoosted && Target is null);
public RoutingMode RoutingMode { get; }

/// <summary>
/// Gets the HTTP method of the current request.
Expand Down Expand Up @@ -75,10 +72,11 @@ public HtmxRequest(HttpContext context)
ArgumentNullException.ThrowIfNull(context);
Method = context.Request.Method;
Path = context.Request.Path;
var ishtmx = IsHtmxRequest = context.Request.Headers.ContainsKey(HtmxRequestHeaderNames.HtmxRequest);
var isHtmx = IsHtmxRequest = context.Request.Headers.ContainsKey(HtmxRequestHeaderNames.HtmxRequest);

if (!ishtmx)
if (!isHtmx)
{
RoutingMode = RoutingMode.Standard;
return;
}

Expand All @@ -90,6 +88,10 @@ public HtmxRequest(HttpContext context)
Trigger = GetHxValueOrDefault(context.Request.Headers, HtmxRequestHeaderNames.Trigger);
Prompt = GetHxValueOrDefault(context.Request.Headers, HtmxRequestHeaderNames.Prompt);
EventHandlerId = GetHxValueOrDefault(context.Request.Headers, HtmxRequestHeaderNames.EventHandlerId);

RoutingMode = !IsHtmxRequest || (IsBoosted && Target is null)
? RoutingMode.Standard
: RoutingMode.Direct;
}

private static string? GetHxValueOrDefault(IHeaderDictionary headers, string key)
Expand Down
29 changes: 29 additions & 0 deletions src/Htmxor/Http/RoutingMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;

namespace Htmxor.Http;

/// <summary>
/// Specifies the routing mode used by Htmxor to handle a HTTP request.
/// </summary>
public enum RoutingMode
{
/// <summary>
/// In standard routing mode is the same as normal Blazor Static Web app
/// page routing, i.e. a request will go through the root component
/// (specified by <see cref="RazorComponentsEndpointRouteBuilderExtensions.MapRazorComponents{TRootComponent}(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder)"/>),
/// and the root component will decide what to do with the request,
/// usually rendering the <see cref="Router"/> component which will
/// inspect the HTTP request and then render the component/page that
/// matches the request, usually wrapped in a <see cref="LayoutComponentBase"/>.
/// </summary>
Standard,

/// <summary>
/// In direct routing mode, Htmxor will route direct to the component/page that
/// matches the request. If the component specifies an <see cref="HtmxLayoutAttribute"/>,
/// the component will be rendered within the layout specified by the attribute.
/// </summary>
Direct,
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
}

public virtual bool ShouldOutput([NotNull] HtmxContext context, int directConditionalChildren, int conditionalChildren)
=> context.Request.IsFullPageRequest;
=> context.Request.RoutingMode is RoutingMode.Standard;
}
<button @onget=OnGet
hx-get="/custom-event-handler"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
}

public virtual bool ShouldOutput([NotNull] HtmxContext context, int directConditionalChildren, int conditionalChildren)
=> context.Request.IsFullPageRequest;
=> context.Request.RoutingMode is RoutingMode.Standard;
}
<form id="checked-contacts"
hx-post="/bulk-update-1"
Expand Down

0 comments on commit 765d4a4

Please sign in to comment.