diff --git a/README.md b/README.md index a3cb588..cdec1a5 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Provides HTMX integration for ASP.NET Core applications. * [HtmxRequestAttribute](#htmxrequestattribute) * [HttpResponse](#httpresponse) * [The declarative way of setting response headers](#the-declarative-way-of-setting-response-headers) + * [Polling](#polling) * [Tag Helpers](#tag-helpers) * [HtmxUrlTagHelper](#htmxurltaghelper) * [HtmxHeaderTagHelper](#htmxheadertaghelper) @@ -360,8 +361,7 @@ Its callback receives an `HtmxResponse`, allowing you to configure response head Response.Htmx(h => h .TriggerEvent( eventName: "process", - detail: new { Value = ... }) - .StopPolling(ShouldStopPolling)); + detail: new { Value = ... })); ``` `TriggerEvent` and `TriggerEvents` accept an optional `HtmxTriggerTiming` value. @@ -377,12 +377,11 @@ the upstream timing change. ```csharp Response.Htmx( - static (h, stop) => h + static (h, value) => h .TriggerEvent( eventName: "process", - detail: new { Value = ... }) - .StopPolling(stop), - ShouldStopPolling); + detail: new { Value = value }), + ProcessValue); ``` :bulb: The same API works in Minimal API handlers by binding `HttpResponse`: @@ -457,6 +456,44 @@ public HtmxSwap Reswap { get; set; } Use `ReswapExpression` when the strongly typed `Reswap` property is not flexible enough. +## Polling + +For server-controlled polling that works in every supported HTMX version, return +the polling element itself and replace it with `outerHTML`: + +```html +
+ Polling... +
+``` + +While polling should continue, return the same element with its request +attributes. To stop, return the element without `hx-get` and `hx-trigger`: + +```html +
+ Polling stopped! +
+``` + +This load-polling pattern gives the server control over every next request and +works with HTMX 1.9.x, 2.x, and 4.x. For an indefinitely updated status, use +`hx-trigger="every 1s"` instead. + +HTMX 1.9.x and 2.x also recognize HTTP status code `286` as a fixed-rate polling +stop signal. Applications that target only those versions can opt into that legacy +behavior directly: + +```csharp +Response.StatusCode = 286; +``` + +HTMX 4.x treats `286` as a regular successful response, so it is not exposed as a +toolkit API. + ## Tag Helpers The library provides five tag helpers: diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml index 94efabc..8ec7251 100644 --- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml @@ -8,25 +8,15 @@

Example 05

Polling

-

Stop a repeating HTMX request from the server when a condition is reached

+

Let the server decide whether to schedule the next HTMX request

-

HtmxResponse.StopPolling()

-

The request is evaluated every second. The server randomly asks HTMX to stop polling.

-
-
- Polling is active +

Server-controlled polling

+

The server returns the next polling element every second until it randomly stops.

-
- Waiting for the first response. -
+
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml.cs index 0d9adc9..1d2b6bd 100644 --- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml.cs +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml.cs @@ -5,15 +5,17 @@ namespace Ramstack.HtmxToolkit.Demo.Pages.Examples; public class PollingModel : PageModel { - public IActionResult OnGetStopPolling() - { - var stop = Random.Shared.Next(0, 20) == 10; - Response.Htmx((h, f) => h.StopPolling(f), stop); + public PollingState State { get; } = new(false, "Polling is active"); - var content = $"Polling... {DateTime.Now:HH:mm:ss}"; - if (stop) - content += "Polling stopped!"; + public IActionResult OnGetPoll() + { + var stopped = Random.Shared.Next(0, 20) == 10; + var message = stopped + ? "Polling stopped!" + : $"Polling... {DateTime.Now:HH:mm:ss}"; - return Content(content); + return Partial("_PollingStatus", new PollingState(stopped, message)); } + + public sealed record PollingState(bool Stopped, string Message); } diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/_PollingStatus.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/_PollingStatus.cshtml new file mode 100644 index 0000000..bed0f74 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/_PollingStatus.cshtml @@ -0,0 +1,18 @@ +@model PollingModel.PollingState + +@if (Model.Stopped) +{ +
+ @Model.Message +
+} +else +{ +
+ @Model.Message +
+} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml index 691dc25..7157d0a 100644 --- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml @@ -13,7 +13,7 @@

02. Route data

Pass route values without query-string assembly.

03. Request headers

Attach custom HTMX headers in Razor.

04. Fluent responses

Configure response directives on the server.

-

05. Polling

Stop repeated requests from a handler.

+

05. Polling

Let the server control repeated requests.

06. HTMX requests

Detect asynchronous requests.

07. Response headers

Override the requested swap strategy.

08. Boosted navigation

Detect links enhanced by HTMX.

diff --git a/samples/Ramstack.HtmxToolkit.Demo/wwwroot/css/demo.css b/samples/Ramstack.HtmxToolkit.Demo/wwwroot/css/demo.css index 4d76be2..0313687 100644 --- a/samples/Ramstack.HtmxToolkit.Demo/wwwroot/css/demo.css +++ b/samples/Ramstack.HtmxToolkit.Demo/wwwroot/css/demo.css @@ -274,6 +274,11 @@ code { font-size: 0.85rem; } +.poll-status--stopped { + background: rgb(255 165 0 / 0.15); + color: orange; +} + .demo-form { display: grid; gap: 1rem; diff --git a/src/Ramstack.HtmxToolkit/HtmxResponse.cs b/src/Ramstack.HtmxToolkit/HtmxResponse.cs index fe965d3..5f8f8d6 100644 --- a/src/Ramstack.HtmxToolkit/HtmxResponse.cs +++ b/src/Ramstack.HtmxToolkit/HtmxResponse.cs @@ -21,11 +21,6 @@ public readonly struct HtmxResponse { private readonly HttpResponse _response; - /// - /// The HTTP status code used by HTMX to stop polling. - /// - public const int StopPollingStatusCode = 286; - /// /// Gets the strongly typed HTMX response headers. /// @@ -252,31 +247,6 @@ static HtmxResponse TriggerEventImpl(HtmxResponse response, string eventName, ob public HtmxResponse TriggerEvents(IReadOnlyDictionary events, HtmxTriggerTiming timing = HtmxTriggerTiming.Receive) => AddEvents(this, events, timing); - /// - /// Sets HTTP status code 286 to stop polling. - /// - /// - /// The current instance. - /// - public HtmxResponse StopPolling() => - StopPolling(true); - - /// - /// Sets HTTP status code 286 when is . - /// - /// to stop polling; - /// otherwise, . - /// - /// The current instance. - /// - public HtmxResponse StopPolling(bool condition) - { - if (condition) - _response.StatusCode = StopPollingStatusCode; - - return this; - } - /// /// Sets a response header and returns the response wrapper for fluent chaining. /// diff --git a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs index 8148ff6..4cc1511 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs @@ -184,33 +184,6 @@ public void Reselect_SetsHeader() Assert.That(context.Response.Headers[HtmxResponseHeaderNames.Reselect], Is.EqualTo("#list")); } - [Test] - public void StopPolling_SetsStatusCode286() - { - var context = TestHelper.CreateHtmxRequestContext(); - context.Response.Htmx(r => r.StopPolling()); - - Assert.That(context.Response.StatusCode, Is.EqualTo(HtmxResponse.StopPollingStatusCode)); - } - - [Test] - public void StopPolling_WithFalseCondition_DoesNotChangeStatusCode() - { - var context = TestHelper.CreateHtmxRequestContext(); - context.Response.Htmx(r => r.StopPolling(false)); - - Assert.That(context.Response.StatusCode, Is.Not.EqualTo(HtmxResponse.StopPollingStatusCode)); - } - - [Test] - public void StopPolling_WithTrueCondition_SetsStatusCode286() - { - var context = TestHelper.CreateHtmxRequestContext(); - context.Response.Htmx(r => r.StopPolling(true)); - - Assert.That(context.Response.StatusCode, Is.EqualTo(HtmxResponse.StopPollingStatusCode)); - } - [Test] public void TriggerEvent_SetsReceiveTrigger() {