From 3576146adf451b0d4d28a7bf6ce52165b9db2ecc Mon Sep 17 00:00:00 2001 From: rameel Date: Mon, 31 Aug 2026 17:34:01 +0500 Subject: [PATCH] Remove StopPolling API HTMX 4 no longer treats HTTP 286 as a special signal to stop polling. Keeping StopPolling would therefore suggest that it works across supported HTMX versions, while it has no effect with HTMX 4. Remove the misleading convenience API and document the supported, version-independent way to stop polling: replace the polling element with a terminal response that omits its polling attributes. For applications that deliberately target HTMX 1.9/2.x, HTTP 286 can still be set manually. --- README.md | 49 ++++++++++++++++--- .../Pages/Examples/Polling.cshtml | 18 ++----- .../Pages/Examples/Polling.cshtml.cs | 18 ++++--- .../Pages/Examples/_PollingStatus.cshtml | 18 +++++++ .../Pages/Index.cshtml | 2 +- .../wwwroot/css/demo.css | 5 ++ src/Ramstack.HtmxToolkit/HtmxResponse.cs | 30 ------------ .../HtmxResponseTests.cs | 27 ---------- 8 files changed, 81 insertions(+), 86 deletions(-) create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/_PollingStatus.cshtml 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() {