From b710fbf0cd8004603e11309656f262a6d45efcc8 Mon Sep 17 00:00:00 2001 From: rameel Date: Mon, 10 Aug 2026 18:53:06 +0500 Subject: [PATCH 1/5] Fix ResponseHandlingEntry children not being collected by HtmxConfigTagHelper --- .../TagHelpers/HtmxConfigTagHelper.cs | 11 +------ .../ResponseHandlingEntryTagHelper.cs | 30 +++++++++---------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs index 5d56999..d048b6f 100644 --- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs +++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs @@ -21,12 +21,6 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper { private readonly IAntiforgery _antiforgery = antiforgery; - /// - /// The key used to pass items between - /// and this tag helper via . - /// - internal const string ResponseHandlingEntriesKey = "Htmx_ResponseHandlingEntries"; - /// /// Gets or sets a value indicating whether history is enabled. /// Defaults to . @@ -372,12 +366,9 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu output.TagMode = TagMode.SelfClosing; output.Attributes.SetAttribute("name", "htmx-config"); + context.Items[typeof(HtmxConfigTagHelper)] = this; await output.GetChildContentAsync(); - if (context.Items.TryGetValue(ResponseHandlingEntriesKey, out var value)) - if (value is List entries && entries.Count != 0) - ResponseHandling = entries; - #if NET8_0_OR_GREATER var config = new HtmlString( JsonSerializer.Serialize( diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntryTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntryTagHelper.cs index f767981..e06a954 100644 --- a/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntryTagHelper.cs +++ b/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntryTagHelper.cs @@ -56,24 +56,22 @@ public sealed class ResponseHandlingEntryTagHelper : TagHelper /// public override void Process(TagHelperContext context, TagHelperOutput output) { - if (!context.Items.TryGetValue(HtmxConfigTagHelper.ResponseHandlingEntriesKey, out var value)) + if (context.Items.TryGetValue(typeof(HtmxConfigTagHelper), out var value)) { - value = new List(); - context.Items[HtmxConfigTagHelper.ResponseHandlingEntriesKey] = value; - } - - if (value is List entries) - { - entries.Add(new ResponseHandlingEntry + if (value is HtmxConfigTagHelper config) { - Code = Code, - Swap = Swap, - Error = Error, - IgnoreTitle = IgnoreTitle, - Select = Select, - Target = Target, - SwapOverride = SwapOverride - }); + config.ResponseHandling ??= new List(); + config.ResponseHandling.Add(new ResponseHandlingEntry + { + Code = Code, + Swap = Swap, + Error = Error, + IgnoreTitle = IgnoreTitle, + Select = Select, + Target = Target, + SwapOverride = SwapOverride + }); + } } output.SuppressOutput(); From c5fc407ade274e8e7b1492f1f7b4206e13a834fa Mon Sep 17 00:00:00 2001 From: rameel Date: Mon, 10 Aug 2026 19:53:00 +0500 Subject: [PATCH 2/5] Delegate tag helper properties to internal data objects Remove #if NET8_0_OR_GREATER around the sourcegen JsonSerializerContext, since all types are now simple POCOs compatible with .NET 6 sourcegen --- .../Ramstack.HtmxToolkit.csproj | 2 +- .../HtmxConfigJsonSerializerContext.cs | 6 +- .../TagHelpers/HtmxConfigTagHelper.cs | 344 ++++++++++++------ .../ResponseHandlingEntryTagHelper.cs | 55 ++- 4 files changed, 275 insertions(+), 132 deletions(-) diff --git a/src/Ramstack.HtmxToolkit/Ramstack.HtmxToolkit.csproj b/src/Ramstack.HtmxToolkit/Ramstack.HtmxToolkit.csproj index f9b5bec..bfb969f 100644 --- a/src/Ramstack.HtmxToolkit/Ramstack.HtmxToolkit.csproj +++ b/src/Ramstack.HtmxToolkit/Ramstack.HtmxToolkit.csproj @@ -1,6 +1,6 @@ - net6.0;net8.0 + net6.0 Enables seamless integration of HTMX with ASP.NET Core (https://htmx.org). enable enable diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigJsonSerializerContext.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigJsonSerializerContext.cs index b1d7520..d02b71a 100644 --- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigJsonSerializerContext.cs +++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigJsonSerializerContext.cs @@ -1,4 +1,3 @@ -#if NET8_0_OR_GREATER using System.Text.Json.Serialization; namespace Ramstack.HtmxToolkit.TagHelpers; @@ -8,8 +7,5 @@ namespace Ramstack.HtmxToolkit.TagHelpers; PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, GenerationMode = JsonSourceGenerationMode.Serialization)] -[JsonSerializable(typeof(HtmxConfigTagHelper.HtmxConfiguration))] -[JsonSerializable(typeof(HtmxConfigTagHelper.AntiForgeryTokenData?))] -[JsonSerializable(typeof(IList))] +[JsonSerializable(typeof(HtmxConfigTagHelper.HtmxConfigData))] internal partial class HtmxConfigJsonSerializerContext : JsonSerializerContext; -#endif diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs index d048b6f..8902d5a 100644 --- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs +++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs @@ -19,7 +19,7 @@ namespace Ramstack.HtmxToolkit.TagHelpers; [HtmlTargetElement("htmx-config", TagStructure = TagStructure.NormalOrSelfClosing)] public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper { - private readonly IAntiforgery _antiforgery = antiforgery; + private readonly HtmxConfigData _config = new(); /// /// Gets or sets a value indicating whether history is enabled. @@ -29,14 +29,22 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// Supported in HTMX 1.x and 2.x. This is mainly useful for testing. /// [HtmlAttributeName("history-enabled")] - public bool? HistoryEnabled { get; set; } + public bool? HistoryEnabled + { + get => _config.HistoryEnabled; + set => _config.HistoryEnabled = value; + } /// /// Gets or sets the size of the history cache. Defaults to 10. /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("history-cache-size")] - public int? HistoryCacheSize { get; set; } + public int? HistoryCacheSize + { + get => _config.HistoryCacheSize; + set => _config.HistoryCacheSize = value; + } /// /// Gets or sets a value indicating whether a full page refresh @@ -45,7 +53,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("refresh-on-history-miss")] - public bool? RefreshOnHistoryMiss { get; set; } + public bool? RefreshOnHistoryMiss + { + get => _config.RefreshOnHistoryMiss; + set => _config.RefreshOnHistoryMiss = value; + } /// /// Gets or sets the default swap style. @@ -53,21 +65,34 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("default-swap-style")] - public HtmxSwap? DefaultSwapStyle { get; set; } + public HtmxSwap? DefaultSwapStyle + { + // NOTE: The getter exists primarily for debugging; performance is not a concern here. + get => EnumHelper.ParseHtmxSwap(_config.DefaultSwapStyle); + set => _config.DefaultSwapStyle = value.GetSwapValue(); + } /// /// Gets or sets the default swap delay. Defaults to 0. /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("default-swap-delay")] - public int? DefaultSwapDelay { get; set; } + public int? DefaultSwapDelay + { + get => _config.DefaultSwapDelay; + set => _config.DefaultSwapDelay = value; + } /// /// Gets or sets the default settle delay. Defaults to 20. /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("default-settle-delay")] - public int? DefaultSettleDelay { get; set; } + public int? DefaultSettleDelay + { + get => _config.DefaultSettleDelay; + set => _config.DefaultSettleDelay = value; + } /// /// Gets or sets a value indicating whether the indicator styles are loaded. @@ -75,42 +100,66 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("include-indicator-styles")] - public bool? IncludeIndicatorStyles { get; set; } + public bool? IncludeIndicatorStyles + { + get => _config.IncludeIndicatorStyles; + set => _config.IncludeIndicatorStyles = value; + } /// /// Gets or sets the indicator class. Defaults to htmx-indicator. /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("indicator-class")] - public string? IndicatorClass { get; set; } + public string? IndicatorClass + { + get => _config.IndicatorClass; + set => _config.IndicatorClass = value; + } /// /// Gets or sets the request class. Defaults to htmx-request. /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("request-class")] - public string? RequestClass { get; set; } + public string? RequestClass + { + get => _config.RequestClass; + set => _config.RequestClass = value; + } /// /// Gets or sets the added class. Defaults to htmx-added. /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("added-class")] - public string? AddedClass { get; set; } + public string? AddedClass + { + get => _config.AddedClass; + set => _config.AddedClass = value; + } /// /// Gets or sets the swapping class. Defaults to htmx-swapping. /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("swapping-class")] - public string? SwappingClass { get; set; } + public string? SwappingClass + { + get => _config.SwappingClass; + set => _config.SwappingClass = value; + } /// /// Gets or sets the settling class. Defaults to htmx-settling. /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("settling-class")] - public string? SettlingClass { get; set; } + public string? SettlingClass + { + get => _config.SettlingClass; + set => _config.SettlingClass = value; + } /// /// Gets or sets a value indicating whether eval is allowed. @@ -118,7 +167,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("allow-eval")] - public bool? AllowEval { get; set; } + public bool? AllowEval + { + get => _config.AllowEval; + set => _config.AllowEval = value; + } /// /// Gets or sets a value indicating whether script tags should be processed in new content. @@ -126,7 +179,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("allow-script-tags")] - public bool? AllowScriptTags { get; set; } + public bool? AllowScriptTags + { + get => _config.AllowScriptTags; + set => _config.AllowScriptTags = value; + } /// /// Gets or sets a value meaning that no nonce will be added to inline scripts. @@ -134,7 +191,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("inline-script-nonce")] - public string? InlineScriptNonce { get; set; } + public string? InlineScriptNonce + { + get => _config.InlineScriptNonce; + set => _config.InlineScriptNonce = value; + } /// /// Gets or sets a value meaning that no nonce will be added to inline styles. @@ -142,7 +203,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported only in HTMX 2.x. [HtmlAttributeName("inline-style-nonce")] - public string? InlineStyleNonce { get; set; } + public string? InlineStyleNonce + { + get => _config.InlineStyleNonce; + set => _config.InlineStyleNonce = value; + } /// /// Gets or sets the attributes to settle during the settling phase. @@ -150,7 +215,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("attributes-to-settle")] - public string[]? AttributesToSettle { get; set; } + public string[]? AttributesToSettle + { + get => _config.AttributesToSettle; + set => _config.AttributesToSettle = value; + } /// /// Gets or sets a value indicating whether HTML template tags should be used for parsing content. @@ -158,14 +227,22 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported only in HTMX 1.x. Removed in HTMX 2.x. [HtmlAttributeName("use-template-fragments")] - public bool? UseTemplateFragments { get; set; } + public bool? UseTemplateFragments + { + get => _config.UseTemplateFragments; + set => _config.UseTemplateFragments = value; + } /// /// Gets or sets the WebSocket reconnect delay. Defaults to full-jitter. /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("ws-reconnect-delay")] - public string? WsReconnectDelay { get; set; } + public string? WsReconnectDelay + { + get => _config.WsReconnectDelay; + set => _config.WsReconnectDelay = value; + } /// /// Gets or sets the type of binary data @@ -173,7 +250,12 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("ws-binary-type")] - public HtmxBinaryType? WsBinaryType { get; set; } + public HtmxBinaryType? WsBinaryType + { + // NOTE: The getter exists primarily for debugging; performance is not a concern here. + get => Enum.TryParse(_config.WsBinaryType ?? "", true, out var v) ? v : null; + set => _config.WsBinaryType = value?.GetWsBinaryTypeValue(); + } /// /// Gets or sets the "disable" selector. @@ -182,7 +264,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("disable-selector")] - public string? DisableSelector { get; set; } + public string? DisableSelector + { + get => _config.DisableSelector; + set => _config.DisableSelector = value; + } /// /// Gets or sets the value that allows cross-site Access-Control requests @@ -191,7 +277,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("with-credentials")] - public bool? WithCredentials { get; set; } + public bool? WithCredentials + { + get => _config.WithCredentials; + set => _config.WithCredentials = value; + } /// /// Gets or sets a value indicating whether htmx attribute inheritance is disabled. @@ -201,7 +291,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported only in HTMX 2.x. [HtmlAttributeName("disable-inheritance")] - public bool? DisableInheritance { get; set; } + public bool? DisableInheritance + { + get => _config.DisableInheritance; + set => _config.DisableInheritance = value; + } /// /// Gets or sets the number of milliseconds a request can take before automatically being terminated. @@ -209,7 +303,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("timeout")] - public int? Timeout { get; set; } + public int? Timeout + { + get => _config.Timeout; + set => _config.Timeout = value; + } /// /// Gets or sets a value indicating the behavior for a boosted link on page transitions. @@ -218,7 +316,12 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("scroll-behavior")] - public HtmxScrollBehavior? ScrollBehavior { get; set; } + public HtmxScrollBehavior? ScrollBehavior + { + // NOTE: The getter exists primarily for debugging; performance is not a concern here. + get => Enum.TryParse(_config.ScrollBehavior ?? "", true, out var v) ? v : null; + set => _config.ScrollBehavior = value?.GetScrollBehaviorValue(); + } /// /// Gets or sets a value indicating whether the focused element should be scrolled into view. @@ -226,7 +329,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("default-focus-scroll")] - public bool? DefaultFocusScroll { get; set; } + public bool? DefaultFocusScroll + { + get => _config.DefaultFocusScroll; + set => _config.DefaultFocusScroll = value; + } /// /// Gets or sets a value indicating whether a cache‑busting parameter @@ -239,7 +346,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// where the target element is appended to the GET request. /// [HtmlAttributeName("get-cache-buster-param")] - public bool? GetCacheBusterParam { get; set; } + public bool? GetCacheBusterParam + { + get => _config.GetCacheBusterParam; + set => _config.GetCacheBusterParam = value; + } /// /// Gets or sets a value indicating whether the @@ -249,7 +360,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("global-view-transitions")] - public bool? GlobalViewTransitions { get; set; } + public bool? GlobalViewTransitions + { + get => _config.GlobalViewTransitions; + set => _config.GlobalViewTransitions = value; + } /// /// Gets or sets a list of HTTP methods that use URL parameters. @@ -263,7 +378,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// [HtmlAttributeName("methods-that-use-url-params")] - public string[]? MethodsThatUseUrlParams { get; set; } + public string[]? MethodsThatUseUrlParams + { + get => _config.MethodsThatUseUrlParams; + set => _config.MethodsThatUseUrlParams = value; + } /// /// Gets or sets a value indicating whether AJAX requests should be allowed only @@ -272,7 +391,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("self-requests-only")] - public bool? SelfRequestsOnly { get; set; } + public bool? SelfRequestsOnly + { + get => _config.SelfRequestsOnly; + set => _config.SelfRequestsOnly = value; + } /// /// Gets or sets a value indicating whether htmx should not update the title of the document @@ -280,7 +403,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("ignore-title")] - public bool? IgnoreTitle { get; set; } + public bool? IgnoreTitle + { + get => _config.IgnoreTitle; + set => _config.IgnoreTitle = value; + } /// /// Gets or sets a value indicating whether the target of a boosted element @@ -290,7 +417,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("scroll-into-view-on-boost")] - public bool? ScrollIntoViewOnBoost { get; set; } + public bool? ScrollIntoViewOnBoost + { + get => _config.ScrollIntoViewOnBoost; + set => _config.ScrollIntoViewOnBoost = value; + } /// /// Gets or sets the cache to store evaluated trigger specifications into, @@ -300,7 +431,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported in HTMX 1.x and 2.x. [HtmlAttributeName("trigger-specs-cache")] - public string? TriggerSpecsCache { get; set; } + public string? TriggerSpecsCache + { + get => _config.TriggerSpecsCache; + set => _config.TriggerSpecsCache = value; + } /// /// Gets or sets the default response handling behavior for HTTP response status codes. @@ -309,7 +444,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported only in HTMX 2.x. [HtmlAttributeName("response-handling")] - public IList? ResponseHandling { get; set; } + public IList? ResponseHandling + { + get => _config.ResponseHandling; + set => _config.ResponseHandling = value; + } /// /// Gets or sets a value indicating whether to process OOB swaps @@ -318,7 +457,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported only in HTMX 2.x. [HtmlAttributeName("allow-nested-oob-swaps")] - public bool? AllowNestedOobSwaps { get; set; } + public bool? AllowNestedOobSwaps + { + get => _config.AllowNestedOobSwaps; + set => _config.AllowNestedOobSwaps = value; + } /// /// Gets or sets a value indicating whether to treat history cache miss @@ -329,7 +472,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported only in HTMX 2.x. [HtmlAttributeName("history-restore-as-hx-request")] - public bool? HistoryRestoreAsHxRequest { get; set; } + public bool? HistoryRestoreAsHxRequest + { + get => _config.HistoryRestoreAsHxRequest; + set => _config.HistoryRestoreAsHxRequest = value; + } /// /// Gets or sets a value indicating whether to report input validation errors @@ -339,7 +486,11 @@ public sealed class HtmxConfigTagHelper(IAntiforgery antiforgery) : TagHelper /// /// Supported only in HTMX 2.x. [HtmlAttributeName("report-validity-of-forms")] - public bool? ReportValidityOfForms { get; set; } + public bool? ReportValidityOfForms + { + get => _config.ReportValidityOfForms; + set => _config.ReportValidityOfForms = value; + } /// /// Gets or sets a value indicating whether an antiforgery token should be included. @@ -369,88 +520,63 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu context.Items[typeof(HtmxConfigTagHelper)] = this; await output.GetChildContentAsync(); - #if NET8_0_OR_GREATER - var config = new HtmlString( - JsonSerializer.Serialize( - new HtmxConfiguration(this), - HtmxConfigJsonSerializerContext.Default.HtmxConfiguration)); - #else + if (IncludeAntiForgeryToken) + _config.AntiForgery = antiforgery.GetAndStoreTokens(ViewContext.HttpContext); + var config = new HtmlString( JsonSerializer.Serialize( - new HtmxConfiguration(this), - JsonOptions.CamelCase)); - #endif + _config, + HtmxConfigJsonSerializerContext.Default.HtmxConfigData)); output.Attributes.SetAttribute( new TagHelperAttribute("content", config, HtmlAttributeValueStyle.SingleQuotes)); } - #region Inner type: HtmxConfiguration - - /// - /// Represents a proxy structure for the class. - /// - [SuppressMessage("ReSharper", "UnusedMember.Local")] - internal readonly struct HtmxConfiguration(HtmxConfigTagHelper helper) - { - public bool? HistoryEnabled => helper.HistoryEnabled; - public int? HistoryCacheSize => helper.HistoryCacheSize; - public bool? RefreshOnHistoryMiss => helper.RefreshOnHistoryMiss; - public string? DefaultSwapStyle => helper.DefaultSwapStyle.GetSwapValue(); - public int? DefaultSwapDelay => helper.DefaultSwapDelay; - public int? DefaultSettleDelay => helper.DefaultSettleDelay; - public bool? IncludeIndicatorStyles => helper.IncludeIndicatorStyles; - public string? IndicatorClass => helper.IndicatorClass; - public string? RequestClass => helper.RequestClass; - public string? AddedClass => helper.AddedClass; - public string? SettlingClass => helper.SettlingClass; - public string? SwappingClass => helper.SwappingClass; - public bool? AllowEval => helper.AllowEval; - public bool? AllowScriptTags => helper.AllowScriptTags; - public string? InlineScriptNonce => helper.InlineScriptNonce; - public string? InlineStyleNonce => helper.InlineStyleNonce; - public string[]? AttributesToSettle => helper.AttributesToSettle; - public bool? UseTemplateFragments => helper.UseTemplateFragments; - public string? WsReconnectDelay => helper.WsReconnectDelay; - public string? WsBinaryType => helper.WsBinaryType?.GetWsBinaryTypeValue(); - public string? DisableSelector => helper.DisableSelector; - public bool? WithCredentials => helper.WithCredentials; - public bool? DisableInheritance => helper.DisableInheritance; - public int? Timeout => helper.Timeout; - public string? ScrollBehavior => helper.ScrollBehavior?.GetScrollBehaviorValue(); - public bool? DefaultFocusScroll => helper.DefaultFocusScroll; - public bool? GetCacheBusterParam => helper.GetCacheBusterParam; - public bool? GlobalViewTransitions => helper.GlobalViewTransitions; - public string[]? MethodsThatUseUrlParams => helper.MethodsThatUseUrlParams; - public bool? SelfRequestsOnly => helper.SelfRequestsOnly; - public bool? IgnoreTitle => helper.IgnoreTitle; - public bool? ScrollIntoViewOnBoost => helper.ScrollIntoViewOnBoost; - public string? TriggerSpecsCache => helper.TriggerSpecsCache; - public IList? ResponseHandling => helper.ResponseHandling; - public bool? AllowNestedOobSwaps => helper.AllowNestedOobSwaps; - public bool? HistoryRestoreAsHxRequest => helper.HistoryRestoreAsHxRequest; - public bool? ReportValidityOfForms => helper.ReportValidityOfForms; - public AntiForgeryTokenData? AntiForgery => GetAntiForgeryToken(helper); - - private static AntiForgeryTokenData? GetAntiForgeryToken(HtmxConfigTagHelper h) => - h.IncludeAntiForgeryToken - ? new AntiForgeryTokenData(h._antiforgery.GetAndStoreTokens(h.ViewContext.HttpContext)) - : null; - } - - #endregion - - #region Inner type: AntiForgeryTokenData + #region Inner type: HtmxConfigData /// - /// Represents a proxy structure for the class. + /// Represents the serializable configuration data for the class. /// - [SuppressMessage("ReSharper", "UnusedMember.Local")] - internal readonly struct AntiForgeryTokenData(AntiforgeryTokenSet antiforgery) + internal sealed class HtmxConfigData { - public string? HeaderName => antiforgery.HeaderName; - public string FormFieldName => antiforgery.FormFieldName; - public string? RequestToken => antiforgery.RequestToken; + public bool? HistoryEnabled { get; set; } + public int? HistoryCacheSize { get; set; } + public bool? RefreshOnHistoryMiss { get; set; } + public string? DefaultSwapStyle { get; set; } + public int? DefaultSwapDelay { get; set; } + public int? DefaultSettleDelay { get; set; } + public bool? IncludeIndicatorStyles { get; set; } + public string? IndicatorClass { get; set; } + public string? RequestClass { get; set; } + public string? AddedClass { get; set; } + public string? SwappingClass { get; set; } + public string? SettlingClass { get; set; } + public bool? AllowEval { get; set; } + public bool? AllowScriptTags { get; set; } + public string? InlineScriptNonce { get; set; } + public string? InlineStyleNonce { get; set; } + public string[]? AttributesToSettle { get; set; } + public bool? UseTemplateFragments { get; set; } + public string? WsReconnectDelay { get; set; } + public string? WsBinaryType { get; set; } + public string? DisableSelector { get; set; } + public bool? WithCredentials { get; set; } + public bool? DisableInheritance { get; set; } + public int? Timeout { get; set; } + public string? ScrollBehavior { get; set; } + public bool? DefaultFocusScroll { get; set; } + public bool? GetCacheBusterParam { get; set; } + public bool? GlobalViewTransitions { get; set; } + public string[]? MethodsThatUseUrlParams { get; set; } + public bool? SelfRequestsOnly { get; set; } + public bool? IgnoreTitle { get; set; } + public bool? ScrollIntoViewOnBoost { get; set; } + public string? TriggerSpecsCache { get; set; } + public IList? ResponseHandling { get; set; } + public bool? AllowNestedOobSwaps { get; set; } + public bool? HistoryRestoreAsHxRequest { get; set; } + public bool? ReportValidityOfForms { get; set; } + public AntiforgeryTokenSet? AntiForgery { get; set; } } #endregion diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntryTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntryTagHelper.cs index e06a954..29faa3d 100644 --- a/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntryTagHelper.cs +++ b/src/Ramstack.HtmxToolkit/TagHelpers/ResponseHandlingEntryTagHelper.cs @@ -11,47 +11,77 @@ namespace Ramstack.HtmxToolkit.TagHelpers; [HtmlTargetElement("response-handling", ParentTag = "htmx-config", TagStructure = TagStructure.WithoutEndTag)] public sealed class ResponseHandlingEntryTagHelper : TagHelper { + private readonly ResponseHandlingEntry _entry = new(); + /// /// Gets or sets a regular expression that will be tested against response status codes. /// [HtmlAttributeName("code")] - public string? Code { get; set; } + public string? Code + { + get => _entry.Code; + set => _entry.Code = value; + } /// /// Gets or sets a value indicating whether the response should be swapped into the DOM. /// [HtmlAttributeName("swap")] - public bool? Swap { get; set; } + public bool? Swap + { + get => _entry.Swap; + set => _entry.Swap = value; + } /// /// Gets or sets a value indicating whether htmx should treat this response as an error. /// [HtmlAttributeName("error")] - public bool? Error { get; set; } + public bool? Error + { + get => _entry.Error; + set => _entry.Error = value; + } /// /// Gets or sets a value indicating whether htmx should ignore title tags in the response. /// [HtmlAttributeName("ignore-title")] - public bool? IgnoreTitle { get; set; } + public bool? IgnoreTitle + { + get => _entry.IgnoreTitle; + set => _entry.IgnoreTitle = value; + } /// /// Gets or sets a CSS selector to use to select content from the response. /// [HtmlAttributeName("select")] - public string? Select { get; set; } + public string? Select + { + get => _entry.Select; + set => _entry.Select = value; + } /// /// Gets or sets a CSS selector specifying an alternative target for the response. /// [HtmlAttributeName("target")] - public string? Target { get; set; } + public string? Target + { + get => _entry.Target; + set => _entry.Target = value; + } /// /// Gets or sets an alternative swap mechanism for the response. /// [HtmlAttributeName("swap-override")] - public string? SwapOverride { get; set; } + public string? SwapOverride + { + get => _entry.SwapOverride; + set => _entry.SwapOverride = value; + } /// public override void Process(TagHelperContext context, TagHelperOutput output) @@ -61,16 +91,7 @@ public override void Process(TagHelperContext context, TagHelperOutput output) if (value is HtmxConfigTagHelper config) { config.ResponseHandling ??= new List(); - config.ResponseHandling.Add(new ResponseHandlingEntry - { - Code = Code, - Swap = Swap, - Error = Error, - IgnoreTitle = IgnoreTitle, - Select = Select, - Target = Target, - SwapOverride = SwapOverride - }); + config.ResponseHandling.Add(_entry); } } From 88c3970052bdbd4a3a447ddeb8fc72fc61663896 Mon Sep 17 00:00:00 2001 From: rameel Date: Mon, 10 Aug 2026 22:32:02 +0500 Subject: [PATCH 3/5] Fix proxy event handler to use detail.value instead of detail --- src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.js | 4 ++-- src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.min.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.js b/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.js index f954ee8..2233f54 100644 --- a/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.js +++ b/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.js @@ -25,9 +25,9 @@ document._r_htmx ||= ((document, htmx) => { }); document.addEventListener("_r_proxy", e => { - for (let kvp of e.detail) + for (let kvp of e.detail.value) { htmx.trigger(e.target, kvp.key, kvp.value); + } }); - return true; })(document, htmx); diff --git a/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.min.js b/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.min.js index 8703d23..671f04a 100644 --- a/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.min.js +++ b/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.min.js @@ -1 +1 @@ -document._r_htmx||=((e,t)=>(e.addEventListener("htmx:afterOnLoad",e=>{if(e.detail.boosted){const r=(new DOMParser).parseFromString(e.detail.xhr.responseText,"text/html").querySelector("meta[name='htmx-config']");r&&(t.config.antiForgery=JSON.parse(r.content).antiForgery)}}),e.addEventListener("htmx:configRequest",e=>{if(!/^get$/i.test(e.detail.verb)){const{headerName:r,formFieldName:a,requestToken:n}=t.config.antiForgery??{};n&&!e.detail.parameters[a]&&(r?e.detail.headers[r]=n:e.detail.parameters[a]=n)}}),e.addEventListener("_r_proxy",e=>{for(let r of e.detail)t.trigger(e.target,r.key,r.value)}),!0))(document,htmx); \ No newline at end of file +document._r_htmx||=((e,t)=>(e.addEventListener("htmx:afterOnLoad",e=>{if(e.detail.boosted){const r=(new DOMParser).parseFromString(e.detail.xhr.responseText,"text/html").querySelector("meta[name='htmx-config']");r&&(t.config.antiForgery=JSON.parse(r.content).antiForgery)}}),e.addEventListener("htmx:configRequest",e=>{if(!/^get$/i.test(e.detail.verb)){const{headerName:r,formFieldName:a,requestToken:n}=t.config.antiForgery??{};n&&!e.detail.parameters[a]&&(r?e.detail.headers[r]=n:e.detail.parameters[a]=n)}}),e.addEventListener("_r_proxy",e=>{for(let r of e.detail.value)t.trigger(e.target,r.key,r.value)}),!0))(document,htmx); \ No newline at end of file From 4e2ee1b5e933a835a2b1e313ea2617de1d22d423 Mon Sep 17 00:00:00 2001 From: rameel Date: Tue, 11 Aug 2026 01:45:28 +0500 Subject: [PATCH 4/5] Add rough usage examples --- Ramstack.HtmxToolkit.slnx | 1 + .../Pages/Index.cshtml | 135 ++++++++++++++++++ .../Pages/Index.cshtml.cs | 96 +++++++++++++ .../Pages/Shared/_Layout.cshtml | 63 ++++++++ .../Pages/_ViewImports.cshtml | 4 + .../Pages/_ViewStart.cshtml | 3 + samples/Ramstack.HtmxToolkit.Demo/Program.cs | 19 +++ .../Properties/launchSettings.json | 13 ++ .../Ramstack.HtmxToolkit.Demo.csproj | 13 ++ .../appsettings.json | 9 ++ 10 files changed, 356 insertions(+) create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml.cs create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Shared/_Layout.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/_ViewImports.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/_ViewStart.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Program.cs create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Properties/launchSettings.json create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Ramstack.HtmxToolkit.Demo.csproj create mode 100644 samples/Ramstack.HtmxToolkit.Demo/appsettings.json diff --git a/Ramstack.HtmxToolkit.slnx b/Ramstack.HtmxToolkit.slnx index ecb0b2f..9f1b330 100644 --- a/Ramstack.HtmxToolkit.slnx +++ b/Ramstack.HtmxToolkit.slnx @@ -9,4 +9,5 @@ + diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml new file mode 100644 index 0000000..2287133 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml @@ -0,0 +1,135 @@ +@page +@model IndexModel + +
+

1. HtmxUrlTagHelper — hx-page + hx-page-handler

+

Uses hx-page and hx-page-handler attributes to generate HTMX URLs:

+ + +
+
+ +
+

2. HtmxUrlTagHelper — route parameters via hx-route-* and hx-all-route-data

+ + +
+
+ +
+

3. HtmxHeaderTagHelper — hx-header-* attributes

+

Custom headers added via hx-header-* attributes. The server reads them and triggers client-side events:

+ +
+ + +
+ +
+

4. Fluent API — Response.Htmx()

+

Setting HTMX response headers via the fluent API in the page handler:

+
+ + + +
+
+
+ +
+

5. Polling with StopPolling()

+

The server randomly stops polling via h.StopPolling(condition):

+
+
+ +
+

6. IsHtmxRequest() — detect HTMX vs full page requests

+

Use Request.IsHtmxRequest() in handlers to return different content:

+ + Open as full page request +
+
+ +
+

7. Response.Htmx() — imperative response headers

+

Setting h.Reswap(HtmxSwap.InnerHtml) programmatically in the handler:

+ +
+
+ +
+

8. IsHtmxBoosted() — detect boosted links

+

Use hx-boost="true" on a link to make it an AJAX request:

+ Click this boosted link +
+
+ +
+

9. Random number (click to load)

+ +
+
+ +
+

10. Antiforgery Token — form submission via hx-post

+

The antiforgery token is automatically included in every HTMX request by + <meta htmx-config include-antiforgery-token="true" /> + and the auto-loaded htmx-toolkit.js script + (@@Html.HtmxAntiforgeryScriptPath()). + No manual token handling required in the form.

+
+

+

+

+ +
+
+
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml.cs new file mode 100644 index 0000000..cfa4119 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml.cs @@ -0,0 +1,96 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Ramstack.HtmxToolkit.Demo.Pages; + +[ValidateAntiForgeryToken] +public class IndexModel : PageModel +{ + public IActionResult OnGet() => + Page(); + + public IActionResult OnGetServerTime() => + Content($"Server time: {DateTime.Now:HH:mm:ss}"); + + public IActionResult OnGetHello() => + Content("Hello from the server!"); + + public IActionResult OnGetGreet(string name) => + Content($"Hello, {name}!"); + + public IActionResult OnGetCustomHeader() + { + Response.Htmx(h => h + .TriggerEvent("customEvent", new { message = "#1 Fired from server!" }) + .TriggerEvent("logEvent", new { message = $"Custom-Header = {Request.Headers["Custom-Header"]}" })); + + return Content("Custom headers sent. Check the console and events.") + .Htmx(h => h + .TriggerEvent("customEvent", new { message = "#2 Fired from server!" }) + .TriggerEvent("customEvent", new { message = "#3 Fired from server!" })); + } + + public IActionResult OnGetReswap() + { + Response.Htmx(h => h.Reswap(HtmxSwap.AfterBegin)); + return Content("

Prepended to top with AfterBegin swap!

"); + } + + public IActionResult OnGetRetarget() + { + Response.Htmx(h => h.Retarget("#fluent-result")); + return Content("Retargeted to #fluent-result!"); + } + + public IActionResult OnGetStopPolling() + { + var stop = Random.Shared.Next(0, 50) == 1; + Response.Htmx((h, f) => h.StopPolling(f), stop); + + return Content(stop + ? "Polling stopped!" + : $"Polling... {DateTime.Now:HH:mm:ss}"); + } + + public IActionResult OnGetPartialOrFull() + { + return Content( + Request.IsHtmxRequest() + ? "Partial response (HTMX request detected via IsHtmxRequest())" + : "Full page response. This wouldn't normally be a Content result, but demonstrates the check."); + } + + public IActionResult OnGetDeclarativeReswap() + { + Response.Htmx(h => h.Reswap(HtmxSwap.InnerHtml)); + return Content("Reswapped with InnerHtml via Response.Htmx(h => h.Reswap(HtmxSwap.InnerHtml))!"); + } + + public IActionResult OnGetBoostedCheck() + { + return Content( + Request.IsHtmxBoosted() + ? "Boosted HTMX request detected!" + : "Non-boosted HTMX request."); + } + + public IActionResult OnGetRandom() => + Content($"Random number: {Random.Shared.Next(1, 100)}"); + + public IActionResult OnPostFormSubmit(ContactForm form) + { + return Content($""" + Form submitted successfully!
+ Name: {form.Name}
+ Email: {form.Email}
+ Message: {form.Message} + """); + } + + public class ContactForm + { + public string? Name { get; set; } + public string? Email { get; set; } + public string? Message { get; set; } + } +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Shared/_Layout.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Shared/_Layout.cshtml new file mode 100644 index 0000000..a172d4a --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Shared/_Layout.cshtml @@ -0,0 +1,63 @@ + + + + + + Ramstack.HtmxToolkit Demo + + + + + + + + + + +

Ramstack.HtmxToolkit Demo

+ @RenderBody() + + + + diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/_ViewImports.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..76aa65a --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@namespace Ramstack.HtmxToolkit.Demo.Pages +@using Ramstack.HtmxToolkit +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Ramstack.HtmxToolkit diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/_ViewStart.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..820a2f6 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Program.cs b/samples/Ramstack.HtmxToolkit.Demo/Program.cs new file mode 100644 index 0000000..9c757de --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Program.cs @@ -0,0 +1,19 @@ +using Ramstack.HtmxToolkit.Builder; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRazorPages(); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); +} + +app.UseStaticFiles(); +app.UseRouting(); +app.MapHtmxAntiforgeryScript(); +app.MapRazorPages(); + +app.Run(); diff --git a/samples/Ramstack.HtmxToolkit.Demo/Properties/launchSettings.json b/samples/Ramstack.HtmxToolkit.Demo/Properties/launchSettings.json new file mode 100644 index 0000000..a50afa1 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "Ramstack.HtmxToolkit.Demo": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Ramstack.HtmxToolkit.Demo.csproj b/samples/Ramstack.HtmxToolkit.Demo/Ramstack.HtmxToolkit.Demo.csproj new file mode 100644 index 0000000..7c12a08 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Ramstack.HtmxToolkit.Demo.csproj @@ -0,0 +1,13 @@ + + + net10.0 + enable + preview + enable + Ramstack.HtmxToolkit.Demo + + + + + + diff --git a/samples/Ramstack.HtmxToolkit.Demo/appsettings.json b/samples/Ramstack.HtmxToolkit.Demo/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From e10669fd76e3e38ae54a26a90822b2494c160545 Mon Sep 17 00:00:00 2001 From: rameel Date: Tue, 11 Aug 2026 23:32:17 +0500 Subject: [PATCH 5/5] Redesign demo examples with dark navigation layout --- .editorconfig | 5 +- .../Pages/Examples/Antiforgery.cshtml | 47 +++ .../Pages/Examples/Antiforgery.cshtml.cs | 23 ++ .../Pages/Examples/Boosted.cshtml | 34 ++ .../Pages/Examples/Boosted.cshtml.cs | 13 + .../Pages/Examples/FluentResponse.cshtml | 44 +++ .../Pages/Examples/FluentResponse.cshtml.cs | 19 + .../Pages/Examples/Headers.cshtml | 33 ++ .../Pages/Examples/Headers.cshtml.cs | 19 + .../Pages/Examples/HtmxRequest.cshtml | 41 ++ .../Pages/Examples/HtmxRequest.cshtml.cs | 13 + .../Pages/Examples/Polling.cshtml | 32 ++ .../Pages/Examples/Polling.cshtml.cs | 19 + .../Pages/Examples/Random.cshtml | 34 ++ .../Pages/Examples/Random.cshtml.cs | 10 + .../Pages/Examples/ResponseHeaders.cshtml | 35 ++ .../Pages/Examples/ResponseHeaders.cshtml.cs | 13 + .../Pages/Examples/RouteData.cshtml | 44 +++ .../Pages/Examples/RouteData.cshtml.cs | 10 + .../Pages/Examples/UrlTagHelper.cshtml | 40 ++ .../Pages/Examples/UrlTagHelper.cshtml.cs | 13 + .../Pages/Index.cshtml | 150 +------- .../Pages/Index.cshtml.cs | 88 +---- .../Pages/Shared/_Layout.cshtml | 106 +++-- .../wwwroot/Icon.png | Bin 0 -> 1591 bytes .../wwwroot/css/demo.css | 364 ++++++++++++++++++ .../wwwroot/js/demo.js | 16 + .../TagHelpers/HtmxConfigTagHelper.cs | 1 - 28 files changed, 987 insertions(+), 279 deletions(-) create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Antiforgery.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Antiforgery.cshtml.cs create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Boosted.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Boosted.cshtml.cs create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/FluentResponse.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/FluentResponse.cshtml.cs create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Headers.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Headers.cshtml.cs create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxRequest.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxRequest.cshtml.cs create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml.cs create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Random.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Random.cshtml.cs create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ResponseHeaders.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ResponseHeaders.cshtml.cs create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RouteData.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RouteData.cshtml.cs create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml.cs create mode 100644 samples/Ramstack.HtmxToolkit.Demo/wwwroot/Icon.png create mode 100644 samples/Ramstack.HtmxToolkit.Demo/wwwroot/css/demo.css create mode 100644 samples/Ramstack.HtmxToolkit.Demo/wwwroot/js/demo.js diff --git a/.editorconfig b/.editorconfig index 94ad698..8c749ed 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,15 +8,12 @@ indent_style = space insert_final_newline = true trim_trailing_whitespace = true -[*.{yml,props,targets,csproj}] +[*.{cshtml,css,yml,props,targets,csproj}] indent_size = 2 [*.min.js] insert_final_newline = false -[*.{tt,ttinclude}] -end_of_line = crlf - [*.{cs,cshtml}] resharper_csharp_max_line_length = 160 diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Antiforgery.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Antiforgery.cshtml new file mode 100644 index 0000000..ef45a74 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Antiforgery.cshtml @@ -0,0 +1,47 @@ +@page +@model AntiforgeryModel +@{ + ViewData["Title"] = "Antiforgery"; +} + +
+
+

Example 10

+

Antiforgery forms

+

Submit HTMX forms while the toolkit automatically supplies the verification token

+
+ +
+
+

hx-post with antiforgery

+

The layout configures token inclusion and the toolkit script attaches it to every HTMX request.

+
+ +
+ + + + + + + +
+ +
+ Submit the form to see the posted values. +
+
+
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Antiforgery.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Antiforgery.cshtml.cs new file mode 100644 index 0000000..47f0b9a --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Antiforgery.cshtml.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Ramstack.HtmxToolkit.Demo.Pages.Examples; + +[ValidateAntiForgeryToken] +public class AntiforgeryModel : PageModel +{ + public IActionResult OnPostFormSubmit(ContactForm form) => + Content($""" + Form submitted successfully!
+ Name: {form.Name}
+ Email: {form.Email}
+ Message: {form.Message} + """); + + public class ContactForm + { + public string? Name { get; set; } + public string? Email { get; set; } + public string? Message { get; set; } + } +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Boosted.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Boosted.cshtml new file mode 100644 index 0000000..271c04b --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Boosted.cshtml @@ -0,0 +1,34 @@ +@page +@model BoostedModel +@{ + ViewData["Title"] = "Boosted requests"; +} + +
+
+

Example 08

+

Detect boosted navigation

+

Identify a request initiated by an hx-boost link

+
+ +
+
+

IsHtmxBoosted()

+

The link is progressively enhanced and its response is placed into the result panel.

+
+ + + +
+ The handler has not been called. +
+
+
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Boosted.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Boosted.cshtml.cs new file mode 100644 index 0000000..e13ac5e --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Boosted.cshtml.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Ramstack.HtmxToolkit.Demo.Pages.Examples; + +public class BoostedModel : PageModel +{ + public IActionResult OnGetBoostedCheck() => + Content( + Request.IsHtmxBoosted() + ? "Boosted HTMX request detected!" + : "Non-boosted HTMX request."); +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/FluentResponse.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/FluentResponse.cshtml new file mode 100644 index 0000000..c23186a --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/FluentResponse.cshtml @@ -0,0 +1,44 @@ +@page +@model FluentResponseModel +@{ + ViewData["Title"] = "Fluent response API"; +} + +
+
+

Example 04

+

Fluent response API

+

Set HTMX response headers from the page handler with Response.Htmx()

+
+ +
+
+

Retarget and reswap

+

The response changes the target or swap mode chosen by the triggering element.

+
+ +
+ + + + +
+ +
+ Try either response directive. +
+
+
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/FluentResponse.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/FluentResponse.cshtml.cs new file mode 100644 index 0000000..ee3982f --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/FluentResponse.cshtml.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Ramstack.HtmxToolkit.Demo.Pages.Examples; + +public class FluentResponseModel : PageModel +{ + public IActionResult OnGetReswap() + { + Response.Htmx(h => h.Reswap(HtmxSwap.AfterBegin)); + return Content("

Prepended to top with AfterBegin swap!

"); + } + + public IActionResult OnGetRetarget() + { + Response.Htmx(h => h.Retarget("#fluent-result")); + return Content("Retargeted to #fluent-result!"); + } +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Headers.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Headers.cshtml new file mode 100644 index 0000000..bf3e030 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Headers.cshtml @@ -0,0 +1,33 @@ +@page +@model HeadersModel +@{ + ViewData["Title"] = "Request headers"; +} + +
+
+

Example 03

+

Request headers

+

Define custom HTMX headers declaratively in Razor

+
+ +
+
+

hx-header-*

+

The handler reads the header and triggers two client events.

+
+
+ +
+ +
No request has been sent.
+
+
+
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Headers.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Headers.cshtml.cs new file mode 100644 index 0000000..813b0c8 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Headers.cshtml.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Ramstack.HtmxToolkit.Demo.Pages.Examples; + +public class HeadersModel : PageModel +{ + public IActionResult OnGetCustomHeader() + { + Response.Htmx(h => h + .TriggerEvent("customEvent", new { message = "#1 Fired from server!" }) + .TriggerEvent("logEvent", new { message = $"Custom-Header = {Request.Headers["Custom-Header"]}" })); + + return Content("Custom headers sent!") + .Htmx(h => h + .TriggerEvent("customEvent", new { message = "#2 Fired from server!" }) + .TriggerEvent("customEvent", new { message = "#3 Fired from server!" })); + } +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxRequest.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxRequest.cshtml new file mode 100644 index 0000000..d29a421 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxRequest.cshtml @@ -0,0 +1,41 @@ +@page +@model HtmxRequestModel +@{ + ViewData["Title"] = "HTMX requests"; +} + +
+
+

Example 06

+

Detect HTMX requests

+

Return an appropriate representation when the handler is called asynchronously or directly

+
+ +
+
+

Request.IsHtmxRequest()

+

Compare a normal browser navigation with an HTMX request to the same handler.

+
+ +
+ + + Open full request + +
+ +
+ Choose a request type. +
+
+
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxRequest.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxRequest.cshtml.cs new file mode 100644 index 0000000..5af5f37 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxRequest.cshtml.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Ramstack.HtmxToolkit.Demo.Pages.Examples; + +public class HtmxRequestModel : PageModel +{ + public IActionResult OnGetPartialOrFull() => + Content( + Request.IsHtmxRequest() + ? "Partial response (HTMX request detected via IsHtmxRequest())" + : "Full page response. This wouldn't normally be a Content result, but demonstrates the check."); +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml new file mode 100644 index 0000000..94efabc --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml @@ -0,0 +1,32 @@ +@page +@model PollingModel +@{ + ViewData["Title"] = "Polling"; +} + +
+
+

Example 05

+

Polling

+

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

+
+ +
+
+

HtmxResponse.StopPolling()

+

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

+
+
+ Polling is active +
+ +
+ 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 new file mode 100644 index 0000000..0d9adc9 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Polling.cshtml.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +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); + + var content = $"Polling... {DateTime.Now:HH:mm:ss}"; + if (stop) + content += "Polling stopped!"; + + return Content(content); + } +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Random.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Random.cshtml new file mode 100644 index 0000000..7afc751 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Random.cshtml @@ -0,0 +1,34 @@ +@page +@model RandomModel +@{ + ViewData["Title"] = "On-demand content"; +} + +
+
+

Example 09

+

Load content on demand

+

A compact baseline HTMX interaction using a Razor Page handler

+
+ +
+
+

Random number

+

Click the button to request a fresh server-generated value.

+
+ +
+ +
+ +
+ No value loaded yet. +
+
+
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Random.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Random.cshtml.cs new file mode 100644 index 0000000..93fae0e --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/Random.cshtml.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Ramstack.HtmxToolkit.Demo.Pages.Examples; + +public class RandomModel : PageModel +{ + public IActionResult OnGetRandom() => + Content($"Random number: {Random.Shared.Next(1, 100)}"); +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ResponseHeaders.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ResponseHeaders.cshtml new file mode 100644 index 0000000..472d7c1 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ResponseHeaders.cshtml @@ -0,0 +1,35 @@ +@page +@model ResponseHeadersModel +@{ + ViewData["Title"] = "Response headers"; +} + +
+
+

Example 07

+

Response swap headers

+

Override an element's HTMX swap strategy in the response handler

+
+ +
+
+

Imperative Reswap

+

The trigger asks for outerHTML, but the server responds with innerHTML.

+
+ +
+ +
+ +
+ The response will remain in this panel. +
+
+
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ResponseHeaders.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ResponseHeaders.cshtml.cs new file mode 100644 index 0000000..d112176 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/ResponseHeaders.cshtml.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Ramstack.HtmxToolkit.Demo.Pages.Examples; + +public class ResponseHeadersModel : PageModel +{ + public IActionResult OnGetDeclarativeReswap() + { + Response.Htmx(h => h.Reswap(HtmxSwap.InnerHtml)); + return Content("Reswapped with InnerHtml via Response.Htmx(h => h.Reswap(HtmxSwap.InnerHtml))!"); + } +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RouteData.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RouteData.cshtml new file mode 100644 index 0000000..fd8c136 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RouteData.cshtml @@ -0,0 +1,44 @@ +@page +@model RouteDataModel +@{ + ViewData["Title"] = "Route parameters"; +} + +
+
+

Example 02

+

Route parameters

+

Pass route data with individual attributes or one Razor dictionary

+
+ +
+
+

hx-route-* and hx-all-route-data

+

Both forms produce a typed Razor Pages URL without composing a query string manually.

+
+ +
+ + +
+ +
+ Choose a route-data strategy. +
+
+
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RouteData.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RouteData.cshtml.cs new file mode 100644 index 0000000..725af70 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RouteData.cshtml.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Ramstack.HtmxToolkit.Demo.Pages.Examples; + +public class RouteDataModel : PageModel +{ + public IActionResult OnGetGreet(string name) => + Content($"Hello, {name}!"); +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml new file mode 100644 index 0000000..1d2f1e9 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml @@ -0,0 +1,40 @@ +@page +@model UrlTagHelperModel +@{ + ViewData["Title"] = "URL Tag Helper"; +} + +
+
+

Example 01

+

URL Tag Helper

+

Generate HTMX URLs with familiar Razor Pages routing attributes

+
+ +
+
+

hx-page + hx-page-handler

+

The toolkit generates the request URL and uses hx-get by default.

+
+ +
+ + +
+ +
+ Run an action to see the response. +
+
+
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml.cs new file mode 100644 index 0000000..5b9599a --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/UrlTagHelper.cshtml.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Ramstack.HtmxToolkit.Demo.Pages.Examples; + +public class UrlTagHelperModel : PageModel +{ + public IActionResult OnGetServerTime() => + Content($"Server time: {DateTime.Now:HH:mm:ss}"); + + public IActionResult OnGetHello() => + Content("Hello from the server!"); +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml index 2287133..54bff4d 100644 --- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml @@ -1,135 +1,23 @@ @page @model IndexModel -
-

1. HtmxUrlTagHelper — hx-page + hx-page-handler

-

Uses hx-page and hx-page-handler attributes to generate HTMX URLs:

- - -
-
- -
-

2. HtmxUrlTagHelper — route parameters via hx-route-* and hx-all-route-data

- - -
-
- -
-

3. HtmxHeaderTagHelper — hx-header-* attributes

-

Custom headers added via hx-header-* attributes. The server reads them and triggers client-side events:

- -
- - -
- -
-

4. Fluent API — Response.Htmx()

-

Setting HTMX response headers via the fluent API in the page handler:

-
- -
-

5. Polling with StopPolling()

-

The server randomly stops polling via h.StopPolling(condition):

-
-
- -
-

6. IsHtmxRequest() — detect HTMX vs full page requests

-

Use Request.IsHtmxRequest() in handlers to return different content:

- - Open as full page request -
-
- -
-

7. Response.Htmx() — imperative response headers

-

Setting h.Reswap(HtmxSwap.InnerHtml) programmatically in the handler:

- -
-
- -
-

8. IsHtmxBoosted() — detect boosted links

-

Use hx-boost="true" on a link to make it an AJAX request:

- Click this boosted link -
-
- -
-

9. Random number (click to load)

- -
-
- -
-

10. Antiforgery Token — form submission via hx-post

-

The antiforgery token is automatically included in every HTMX request by - <meta htmx-config include-antiforgery-token="true" /> - and the auto-loaded htmx-toolkit.js script - (@@Html.HtmxAntiforgeryScriptPath()). - No manual token handling required in the form.

-
-

-

-

- -
-
-
+ diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml.cs index cfa4119..c0cddd9 100644 --- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml.cs +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml.cs @@ -1,96 +1,10 @@ -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace Ramstack.HtmxToolkit.Demo.Pages; -[ValidateAntiForgeryToken] public class IndexModel : PageModel { - public IActionResult OnGet() => - Page(); - - public IActionResult OnGetServerTime() => - Content($"Server time: {DateTime.Now:HH:mm:ss}"); - - public IActionResult OnGetHello() => - Content("Hello from the server!"); - - public IActionResult OnGetGreet(string name) => - Content($"Hello, {name}!"); - - public IActionResult OnGetCustomHeader() - { - Response.Htmx(h => h - .TriggerEvent("customEvent", new { message = "#1 Fired from server!" }) - .TriggerEvent("logEvent", new { message = $"Custom-Header = {Request.Headers["Custom-Header"]}" })); - - return Content("Custom headers sent. Check the console and events.") - .Htmx(h => h - .TriggerEvent("customEvent", new { message = "#2 Fired from server!" }) - .TriggerEvent("customEvent", new { message = "#3 Fired from server!" })); - } - - public IActionResult OnGetReswap() - { - Response.Htmx(h => h.Reswap(HtmxSwap.AfterBegin)); - return Content("

Prepended to top with AfterBegin swap!

"); - } - - public IActionResult OnGetRetarget() - { - Response.Htmx(h => h.Retarget("#fluent-result")); - return Content("Retargeted to #fluent-result!"); - } - - public IActionResult OnGetStopPolling() - { - var stop = Random.Shared.Next(0, 50) == 1; - Response.Htmx((h, f) => h.StopPolling(f), stop); - - return Content(stop - ? "Polling stopped!" - : $"Polling... {DateTime.Now:HH:mm:ss}"); - } - - public IActionResult OnGetPartialOrFull() - { - return Content( - Request.IsHtmxRequest() - ? "Partial response (HTMX request detected via IsHtmxRequest())" - : "Full page response. This wouldn't normally be a Content result, but demonstrates the check."); - } - - public IActionResult OnGetDeclarativeReswap() - { - Response.Htmx(h => h.Reswap(HtmxSwap.InnerHtml)); - return Content("Reswapped with InnerHtml via Response.Htmx(h => h.Reswap(HtmxSwap.InnerHtml))!"); - } - - public IActionResult OnGetBoostedCheck() - { - return Content( - Request.IsHtmxBoosted() - ? "Boosted HTMX request detected!" - : "Non-boosted HTMX request."); - } - - public IActionResult OnGetRandom() => - Content($"Random number: {Random.Shared.Next(1, 100)}"); - - public IActionResult OnPostFormSubmit(ContactForm form) - { - return Content($""" - Form submitted successfully!
- Name: {form.Name}
- Email: {form.Email}
- Message: {form.Message} - """); - } - - public class ContactForm + public void OnGet() { - public string? Name { get; set; } - public string? Email { get; set; } - public string? Message { get; set; } } } diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Shared/_Layout.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Shared/_Layout.cshtml index a172d4a..fb0b84e 100644 --- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Shared/_Layout.cshtml +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Shared/_Layout.cshtml @@ -1,63 +1,57 @@ - - - Ramstack.HtmxToolkit Demo - - - - - - - - + + + @(ViewData["Title"] ?? "Home") — Ramstack.HtmxToolkit + + + + + + + + + + -

Ramstack.HtmxToolkit Demo

- @RenderBody() - - + + + + + diff --git a/samples/Ramstack.HtmxToolkit.Demo/wwwroot/Icon.png b/samples/Ramstack.HtmxToolkit.Demo/wwwroot/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a623b058b026ef56cad73c0c05717040338468e1 GIT binary patch literal 1591 zcmV-72FUq|P)C00001b5ch_0olnc ze*gdg32;bRa{vGf6951U69E94oEQKA0wPdMR7D>jA15|bE=hYWNP8e6Cpu}tJ#yDI zU8yNMUNl&pA0Q$kC^S8B(=JPbJ8Q`^R+uI;O*UVyH)OaZEb%SXfxY!^18vE-OQCJaN_}EIsh>@YdGWY;0^HAt7U9W6sXb|Ns9rTcqFL-*n05K|w)0JUpbNr1|;zB{4_d-rl;p zx=l?@L_|cdudn?4{Ij#ORaI3tW3|oA&Hn!W>gwu{kdQPqG)6{7si~>=_xD;_TE)f1 zhK7dX;^NrY*dQPvWo2d1(9o)?sxM83!NI}d;o&7EB_bjsaBy(h+1cdeg@x_y?dIm@ zjEsy~Sy}q}`YI5;?) zo163V^R>0LD=RCAh=}#|^=)l!z`($0XlRs_l)b&ZC@3g0Qj)8yt7&Oz&(F_PR8+aS zx#{WYcGvoE000B7NklKqwb| z4?p1+4C7-U9P3B$(T@}c5XHwpG}e!a<)R-KgAGJWe~x#pNNx zcbR-fCPGvL5!k?L;%6N~S!=Rgp`NZ^ixAy_7z69%1`t}mVdHY;vvYD0QZqn{0o}iC zd7CybM?QZ`0Ya(<#2DC0F6i*a!fi!Ml`l4wAjB{r#y}j!079kPcPvG|sH_knmH~PU z=%kyCJIkGw-&IkG5Zizl1G{A_d&up`ROO6(_1=BNZez`UAvALCOc397NJX(&l5gSU| zn^daB7&uC8n9)IwvooiQ-mv@FaiaKy)uw8f5(6@NdQTEv3r^)L7j~T{=aqPdl_r!J za3KFId0{1nVud1ey|kwA+P#ZHXVD&8iOy^57!|7sAgQ+J+I-hW6j``r-TLL$TPJuq$Fbl z&spj=GZ~G;6SRZci$Gl_@}@~6Ifg097-EGO2v(Dyw&s%8 z)WQ6V&%Bu>x3nsbAxek=`Wv}r;H~U(CaYQU3(SpqlAM~WOXqR;;F*tWVZ}})|K%NOhg&3eUAP%J{OYIaup}c*ZYCuB_ zNDZ`oVn5fEX#A|B81TaSG1M3kw$^>29B8%hb)r+rK!EUF@*8!l-GK4C({BVM=|6rt z8qg90iw0)PXy<8X`1QMAWkws{(SV=G2mu|7~u3_R*ZopJ_a { + const menu_toggle = document.querySelector("[data-menu-toggle]"); + menu_toggle.addEventListener("click", () => document.body.classList.toggle("sidebar-open")); + + document.querySelectorAll(".nav-link").forEach(link => { + link.pathname === window.location.pathname && link.classList.add("active"); + }); + + document.addEventListener("customEvent", e => append_event("Custom event", e.detail.message)); + document.addEventListener("logEvent", e => append_event("Log event", e.detail.message)); + + function append_event(name, message) { + const el = document.querySelector("#event-log"); + el.insertAdjacentHTML("beforeend", `

${name}: ${message}

`); + } +})(); diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs index 8902d5a..56bcfcf 100644 --- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs +++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxConfigTagHelper.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Text.Json; using Microsoft.AspNetCore.Antiforgery;