diff --git a/Readme.md b/Readme.md index a085c00..6e89b27 100644 --- a/Readme.md +++ b/Readme.md @@ -139,30 +139,46 @@ The resulting HTML will be. ### HTMX and Anti-forgery Tokens -You can set the attribute `includeAspNetAntiforgerToken` on the `htmx-config` element. Then you'll need to include this additional JavaScript in your web application. +You can set the attribute `includeAspNetAntiforgerToken` on the `htmx-config` element. Then you'll need to include this additional JavaScript in your web application. We include the attribute `__htmx_antiforgery` to track the event listener was added already. This keeps us from accidentally re-registering the event listener. ```javascript -document.addEventListener("htmx:configRequest", (evt) => { - let httpVerb = evt.detail.verb.toUpperCase(); - if (httpVerb === 'GET') return; - - let antiForgery = htmx.config.antiForgery; - - if (antiForgery) { - - // already specified on form, short circuit - if (evt.detail.parameters[antiForgery.formFieldName]) - return; - - if (antiForgery.headerName) { - evt.detail.headers[antiForgery.headerName] - = antiForgery.requestToken; - } else { - evt.detail.parameters[antiForgery.formFieldName] - = antiForgery.requestToken; +if (!document.body.attributes.__htmx_antiforgery) { + document.addEventListener("htmx:configRequest", evt => { + let httpVerb = evt.detail.verb.toUpperCase(); + if (httpVerb === 'GET') return; + let antiForgery = htmx.config.antiForgery; + if (antiForgery) { + // already specified on form, short circuit + if (evt.detail.parameters[antiForgery.formFieldName]) + return; + + if (antiForgery.headerName) { + evt.detail.headers[antiForgery.headerName] + = antiForgery.requestToken; + } else { + evt.detail.parameters[antiForgery.formFieldName] + = antiForgery.requestToken; + } } - } -}); + }); + document.addEventListener("htmx:afterOnLoad", evt => { + if (evt.detail.boosted) { + const parser = new DOMParser(); + const html = parser.parseFromString(evt.detail.xhr.responseText, 'text/html'); + const selector = 'meta[name=htmx-config]'; + const config = html.querySelector(selector); + if (config) { + const current = document.querySelector(selector); + // only change the anti-forgery token + const key = 'antiForgery'; + htmx.config[key] = JSON.parse(config.attributes['content'].value)[key]; + // update DOM, probably not necessary, but for sanity's sake + current.replaceWith(config); + } + } + }); + document.body.attributes.__htmx_antiforgery = true; +} ``` You can access the snippet in two ways. The first is to use the `HtmxSnippet` static class in your views. @@ -183,6 +199,37 @@ This html helper will result in a ` + + + + + +``` ## License diff --git a/src/Htmx.TagHelpers/HtmlExtensions.cs b/src/Htmx.TagHelpers/HtmlExtensions.cs index 0a080cb..9ce8914 100644 --- a/src/Htmx.TagHelpers/HtmlExtensions.cs +++ b/src/Htmx.TagHelpers/HtmlExtensions.cs @@ -13,6 +13,8 @@ public static class HtmlExtensions /// /// /// Note: This includes wrapping script tags.To get the JavaScript string use HtmxSnippets.AntiforgeryJavaScript. + /// + /// You may also want to consider using the instead of this approach. /// /// An instance of the HTML Helper interface /// HTML Content with JavaScript tag diff --git a/src/Htmx.TagHelpers/HtmxAntiforgeryScriptEndpoints.cs b/src/Htmx.TagHelpers/HtmxAntiforgeryScriptEndpoints.cs new file mode 100644 index 0000000..ea288cf --- /dev/null +++ b/src/Htmx.TagHelpers/HtmxAntiforgeryScriptEndpoints.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Htmx.TagHelpers; + +public static class HtmxAntiforgeryScriptEndpoints +{ + /// + /// The path to the antiforgery script that is used from HTML + /// + public static string Path { get; private set; } = "_htmx/antiforgery.js"; + + /// + /// Register an endpoint that responds with the HTMX antiforgery script.
+ /// IMPORTANT: Remember to add the following script tag to your _Layout.cshtml or Razor view: + /// + /// ]]> + ///
+ /// Endpoint builder + /// The path to the antiforgery script + /// The registered endpoint (Use to reference endpoint) + public static IEndpointConventionBuilder MapHtmxAntiforgeryScript( + this IEndpointRouteBuilder builder, + string? path = null) + { + // set Path globally for access + Path = path ?? Path; + + return builder.MapGet(Path, async ctx => + { + ctx.Response.ContentType = "text/javascript"; + await ctx.Response.WriteAsync(HtmxSnippets.AntiforgeryJavaScript); + }); + } +} \ No newline at end of file diff --git a/src/Htmx.TagHelpers/JavaScript/antiforgerySnippet.js b/src/Htmx.TagHelpers/JavaScript/antiforgerySnippet.js index 596257b..892924d 100644 --- a/src/Htmx.TagHelpers/JavaScript/antiforgerySnippet.js +++ b/src/Htmx.TagHelpers/JavaScript/antiforgerySnippet.js @@ -2,12 +2,9 @@ if (!document.body.attributes.__htmx_antiforgery) { document.addEventListener("htmx:configRequest", evt => { let httpVerb = evt.detail.verb.toUpperCase(); if (httpVerb === 'GET') return; - let antiForgery = htmx.config.antiForgery; - if (antiForgery) { - - // already specified on form, short circuit + // already specified on the form, short circuit if (evt.detail.parameters[antiForgery.formFieldName]) return; @@ -20,5 +17,21 @@ if (!document.body.attributes.__htmx_antiforgery) { } } }); + document.addEventListener("htmx:afterOnLoad", evt => { + if (evt.detail.boosted) { + const parser = new DOMParser(); + const html = parser.parseFromString(evt.detail.xhr.responseText, 'text/html'); + const selector = 'meta[name=htmx-config]'; + const config = html.querySelector(selector); + if (config) { + const current = document.querySelector(selector); + // only change the anti-forgery token + const key = 'antiForgery'; + htmx.config[key] = JSON.parse(config.attributes['content'].value)[key]; + // update DOM, probably not necessary, but for sanity's sake + current.replaceWith(config); + } + } + }); document.body.attributes.__htmx_antiforgery = true; } \ No newline at end of file diff --git a/test/Sample/Pages/Shared/_Layout.cshtml b/test/Sample/Pages/Shared/_Layout.cshtml index a1ccb66..92afd41 100644 --- a/test/Sample/Pages/Shared/_Layout.cshtml +++ b/test/Sample/Pages/Shared/_Layout.cshtml @@ -3,18 +3,23 @@ - + @ViewData["Title"] - Htmx.Sample + + + + - +
-