Giter VIP home page Giter VIP logo

htmx.net's Introduction

Htmx.Net

HTMX Logo

This is a package designed to add server side helper methods for HttpRequest and HttpResponse. This makes working with htmx server-side concepts simpler. You should also consider reading about Hyperscript, an optional companion project for HTMX.

If you're new to HTMX, checkout out this series on getting started with HTMX for ASP.NET Core developer which also includes a sample project and patterns that you might find helpful.

Htmx Extension Methods

Getting Started

Install the Htmx NuGet package to your ASP.NET Core project.

dotnet add package Htmx

HttpRequest

Using the HttpRequest, we can determine if the request was initiated by Htmx on the client.

httpContext.Request.IsHtmx()

This can be used to either return a full page response or a partial page render.

// in a Razor Page
return Request.IsHtmx()
    ? Partial("_Form", this)
    : Page();

We can also retrieve the other header values htmx might set.

Request.IsHtmx(out var values);

Read more about the other header values on the official documentation page.

Browser Caching

As a special note, please be mindful that if your server can render different content for the same URL depending on some other headers, you need to use the Vary response HTTP header. For example, if your server renders the full HTML when Request.IsHtmx() is false, and it renders a fragment of that HTML when Request.IsHtmx() is true, you need to add Vary: HX-Request. That causes the cache to be keyed based on a composite of the response URL and the HX-Request request header — rather than being based just on the response URL.

// in a Razor Page
if (Request.IsHtmx())
{
  Response.Headers.Add("Vary", "HX-Request");
  return Partial("_Form", this)
}

return Page();

HttpResponse

We can set Http Response headers using the Htmx extension method, which passes an action and HtmxResponseHeaders object.

Response.Htmx(h => {
    h.PushUrl("/new-url")
     .WithTrigger("cool")
});

Read more about the HTTP response headers at the official documentation site.

Triggering Client-Side Events

You can trigger client side events with HTMX using the HX-Trigger header. Htmx.Net provides a WithTrigger helper method to configure one or more events that you wish to trigger.

Response.Htmx(h => {
    h.WithTrigger("yes")
     .WithTrigger("cool", timing: HtmxTriggerTiming.AfterSettle)
     .WithTrigger("neat", new { valueForFrontEnd= 42, status= "Done!" }, timing: HtmxTriggerTiming.AfterSwap);
});

CORS Policy

By default, all Htmx requests and responses will be blocked in a cross-origin context.

If you configure your application in a cross-origin context, then setting a CORS policy in ASP.NET Core also allows you to define specific restrictions on request and response headers, enabling fine-grained control over the data that can be exchanged between your web application and different origins.

This library provides a simple approach to exposing Htmx headers to your CORS policy:

var  MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy  =>
                      {
                          policy.WithOrigins("http://example.com", "http://www.contoso.com")
                                   .WithHeaders(HtmxRequestHeaders.Keys.All)  // Add htmx request headers
                                   .WithExposedHeaders(HtmxResponseHeaders.Keys.All)  // Add htmx response headers
                      });
});

Htmx.TagHelpers

Getting Started

Install the Htmx.TagHelpers NuGet package to your ASP.NET Core project. Targets .NET Core 3.1+ projects.

dotnet add package Htmx.TagHelpers

Make the Tag Helpers available in your project by adding the following line to your _ViewImports.cshtml:

@addTagHelper *, Htmx.TagHelpers

You'll generally need URL paths pointing back to your ASP.NET Core backend. Luckily, Htmx.TagHelpers mimics the url generation included in ASP.NET Core. This makes linking HTMX with your ASP.NET Core application a seamless experience.

<div hx-target="this">
    <button hx-get
            hx-page="Index"
            hx-page-handler="Snippet"
            hx-swap="outerHtml">
        Click Me (Razor Page w/ Handler)
    </button>
</div>

<div hx-target="this">
    <button hx-get
            hx-controller="Home"
            hx-action="Index"
            hx-route-id="1">
        Click Me (Controller)
    </button>
</div>

<div hx-target="this">
    <button hx-post
            hx-route="named">
        Click Me (Named)
    </button>
</div>

Htmx.Config

An additional htmx-config tag helper is included that can be applied to a meta element in your page's head that makes creating HTMX configuration simpler. For example, below we can set the historyCacheSize, default indicatorClass, and whether to include ASP.NET Core's anti-forgery tokens as an additional element on the HTMX configuration.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta name="htmx-config" 
          historyCacheSize="20"
          indicatorClass="htmx-indicator"
          includeAspNetAntiforgeryToken="true"
          />
    <!-- additional elements... -->
</head>

The resulting HTML will be.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta name="htmx-config" content='{"indicatorClass":"htmx-indicator","historyCacheSize":20,"antiForgery":{"formFieldName":"__RequestVerificationToken","headerName":"RequestVerificationToken","requestToken":"<token>"}}' />
    <!-- additional elements... -->
</head>

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. We include the attribute __htmx_antiforgery to track the event listener was added already. This keeps us from accidentally re-registering the event listener.

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.

<script>
@Html.Raw(HtmxSnippets.AntiforgeryJavaScript)
</script>

A simpler way is to use the HtmlExtensions class that extends IHtmlHelper.

@Html.HtmxAntiforgeryScript()

This html helper will result in a <script> tag along with the previously mentioned JavaScript. Note: You can still register multiple event handlers for htmx:configRequest, so having more than one is ok.

Note that if the hx-[get|post|put] attribute is on a <form ..> tag and the <form> element has a method="post" (and also an empty or missing action="") attribute, the ASP.NET Tag Helpers will add the Anti-forgery Token as an input element and you do not need to further configure your requests as above. You could also use hx-include pointing to a form, but this all comes down to a matter of preference.

Additionally, and the recommended approach is to use the HtmxAntiforgeryScriptEndpoint, which will let you map the JavaScript file to a specific endpoint, and by default it will be _htmx/antiforgery.js.

app.UseAuthorization();
// registered here
app.MapHtmxAntiforgeryScript();
app.MapRazorPages();
app.MapControllers();

You can now configure this endpoint with caching, authentication, etc. More importantly, you can use the script in your head tag now by applying the defer tag, which is preferred to having JavaScript at the end of a body element.

<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta
        name="htmx-config"
        historyCacheSize="20"
        indicatorClass="htmx-indicator"
        includeAspNetAntiforgeryToken="true"/>
    <title>@ViewData["Title"] - Htmx.Sample</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/>
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>
    <script src="~/lib/jquery/dist/jquery.min.js" defer></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js" defer></script>
    <script src="https://unpkg.com/htmx.org@@1.9.2" defer></script>
    <!-- this uses the static value in a script tag -->
    <script src="@HtmxAntiforgeryScriptEndpoints.Path" defer></script>
</head>

License

Copyright © 2022 Khalid Abuhakmeh

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

htmx.net's People

Contributors

alexzeitler avatar bingzer avatar jerriep avatar jzebedee avatar khalidabuhakmeh avatar maartenba avatar pthomson avatar rudiv avatar seangwright avatar tanczosm avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

htmx.net's Issues

Question: How do I convert the asp-route-returnUrl Tag Helper?

On my <form> elements (for example, a Login form), I use the ASPNET Core return-url Tag Helper

<form
  method="post"
  asp-action="Login"
  asp-route-returnUrl='@Context.Request.Query["ReturnUrl"]'>

It will append this value as a querystring ?returnUrl=... on the generated action attribute value.

What would be the best way to use this with hx-action and hx-post on a <form>?

Potential incorrect caching if using package as documented

In the documentation there is a snippet as follows:

// in a Razor Page
return Request.IsHtmx()
    ? Partial("_Form", this)
    : Page();

In the documentation for HTMX there is a section called caching, which addresses an issue a user of this library will probably run into.

HTMX caching

... Be mindful that if your server can render different content for the same URL depending on some other headers, you need to use the Vary response HTTP header. For example, if your server renders the full HTML when the HX-Request header is missing or false, and it renders a fragment of that HTML when HX-Request: true, you need to add Vary: HX-Request. That causes the cache to be keyed based on a composite of the response URL and the HX-Request request header — rather than being based just on the response URL.

The general behavior would be that you load a partial from the server on one request and if that partial pushes to the history then you'll get the partial as the only page content instead of the actual page. To avoid the browser caching the HTMX response needs to have that header added.

if (Request.IsHtmx())
{
  Response.Headers.Add("Vary", "HX-Request");
  return Partial("_Form", this)
}

return Page();

I'm not sure how this would be addressed within the scope of this library, but it's going to be an issue if the conditional full/partial page gets pushed to history.

Antiforgery header is not sent with hx-delete

Hi,

Your antiforgery snippet has an issue with the shortcut path when the page contains an unrelated form.

In my case the page contains both a form to update an entity and a list of other (sub-)entities for which I want to be able to delete each of those. When the hx-delete is requested, the snippet is triggered but it verifies that there is an antiforgery hidden field in the form and thus doesn't add the required header.

I worked around this by changing the problematic line to
if (httpVerb === 'POST' && evt.detail.parameters[antiForgery.formFieldName])

This fixes my immediate problem but seems to be too simplistic and most probably doesn't handle all the cases.

TagHelper is not working when it has a razor commented attribute

In your sample if you take the first button

<button hx-get
        hx-page="Index"
        hx-page-handler="Snippet"
        hx-swap="outerHtml">
    Click Me (Razor Page w/ Handler)
</button>

It will be rendered as this

<button hx-swap="outerHtml"
        hx-get="/?handler=Snippet">
    Click Me (Razor Page w/ Handler)
</button>

This will also work

<button x-on:keyup.escape.stop="open = false"
        hx-get
        hx-page="Index"
        hx-page-handler="Snippet"
        hx-swap="outerHtml">
    Click Me (Razor Page w/ Handler)
</button>

It will be rendered as this

<button x-on:keyup.escape.stop="open = false" 
        hx-swap="outerHtml" 
        hx-get="/?handler=Snippet">
    Click Me (Razor Page w/ Handler)
</button>

If you change this button like that

<button @* class="" *@
        hx-get
        hx-page="Index"
        hx-page-handler="Snippet"
        hx-swap="outerHtml">
    Click Me (Razor Page w/ Handler)
</button>

or, if you use Alpine.js short syntax the issue is also present

<button @@keyup.escape.stop="open = false"
        hx-get
        hx-page="Index"
        hx-page-handler="Snippet"
        hx-swap="outerHtml">
    Click Me (Razor Page w/ Handler)
</button>

There is no issue with your tag helper, sorry for that. If up place the comment or the alpine statement at the end you have a compilation error.

Dynamically update hx-post

Hi there,

Just wondering if there any way to dynamically change the hx-post? I did change it in javascript using setAttribute but even though the url updates it keeps executing the original url?

Similar to this
bigskysoftware/htmx#882

Any ideas would be appreciated

Deprecated ASP.NET Core package references

Htmx.Net should not reference the old and deprecated 2.x versions of ASP.NET Core packages. Instead, it should use a FrameworkReference entry to target the currently-installed framework.

See Use the ASP.NET Core shared framework:

With the release of .NET Core 3.0, many ASP.NET Core assemblies are no longer published to NuGet as packages. Instead, the assemblies are included in the Microsoft.AspNetCore.App shared framework, which is installed with the .NET Core SDK and runtime installers. For a list of packages no longer being published, see Remove obsolete package references.

As of .NET Core 3.0, projects using the Microsoft.NET.Sdk.Web MSBuild SDK implicitly reference the shared framework. Projects using the Microsoft.NET.Sdk or Microsoft.NET.Sdk.Razor SDK must reference ASP.NET Core to use ASP.NET Core APIs in the shared framework.

hx-post posting form inside a bootstrap modal

Hi there,
Thank you for providing such a great library.

I noticed when I try and use a form inside a bootstrap model, the hx-post doesn't work?

<form hx-post="/hello">
   <button class="btn btn-primary" type="submit">hello</button>
   @Html.HtmxAntiforgeryScript()
</form>

It tries to post the original page rather than the modal dialog form. Is there some special scripting I need to do?

This is an example of something I'm trying to achieve
https://blog.benoitblanchon.fr/django-htmx-modal-form/

Any exmaple or idea on how to fix this?

Bug: SourceLink packages but no symbols generated

If you look at a published NuGet package for this project, you can see there's no index symbols from SourceLink.

That's because the necessary MSBuild props are not set - globally (there's no Directory.Build.props) or per project

Also, when running dotnet pack you need to specify --include-symbols to ensure the .snupkg is generated with the .nupkg.

The symbol package will automatically be sent to NuGet when pushing to NuGet.

I can make a PR to update the project config to make sure the symbols are generated and published so that SourceLink actually works.


There's other MSBuild config you can include to ensure your package validates with NuGet Package Explorer:

image

Example: https://nuget.info/packages/XperienceCommunity.PreviewComponentOutlines/1.0.0

(see: my Directory.Build.props for that project)

Issue with `HtmxAntiforgeryScript` and `hx-boost`

I've spotted a potential issue with HtmxAntiforgeryScript when hx-boost is set to true.

If you modify you HTMX ToDo <body> tag to

<body hx-boost="true">

You start the application on first load, you open de dev console and type the following command :

getEventListeners(window.document)

You'll have 1 handler on htmx:configRequest

image

Now click several times on the menu and type the same command in the dev console

The handlers are stacking on the event

image

Is this an issue or is it ok to have the handlers stacking ? (other events are also stacking)

Rider not highlighting `hx-controller` and `hx-action` attributes

I'm not sure if this is an Htmx.TagHelpers issue or a JetBrains Rider issue. I am using the Htmx.TagHelpers and they work great. Razor converts the following code into an hx-get="/actual/path/to/controller".

<select class="form-control custom-select"
        asp-for="SchoolId"
        asp-items="Model.Schools"
        hx-get
        hx-controller="SurveySubmissions"
        hx-action="GetTeachersAjax"
        hx-route-surveyId="@Model.SurveyId"
        hx-route-currentTeacherSelection="@Model.TargetId"
        hx-target="#teachers"
        hx-indicator=".htmx-indicator">
    <option value="">Select a School&hellip;</option>
</select>

However, Rider does not pick up the hx-controller and hx-action attributes as tag helper enhanced. Looking at the code for the tag helper, I see they are defined using square brackets.

[HtmlTargetElement("*", Attributes = "[hx-get]")]
[HtmlTargetElement("*", Attributes = "[hx-post]")]
[HtmlTargetElement("*", Attributes = "[hx-delete]")]
[HtmlTargetElement("*", Attributes = "[hx-put]")]
[HtmlTargetElement("*", Attributes = "[hx-patch]")]
public class HtmxUrlTagHelper : TagHelper

If I instead change my code to include the brackets, then Rider bolds the attributes but Razor no longer processes them.

<select class="form-control custom-select"
        asp-for="SchoolId"
        asp-items="Model.Schools"
        [hx-get]
        hx-controller="SurveySubmissions"
        hx-action="GetTeachersAjax"
        hx-route-surveyId="@Model.SurveyId"
        hx-route-currentTeacherSelection="@Model.TargetId"
        hx-target="#teachers"
        hx-indicator=".htmx-indicator">
    <option value="">Select a School&hellip;</option>
</select>

Htmx.Net doesn't work in a cross origin context without a CORS policy

When using htmx in a cross origin context, the server must set Access-Control headers in order for htmx headers to be visible on the client side. See https://htmx.org/docs/#cors

Specifically the server end would need to set the response header "Access-Control-Expose-Headers" to a comma-delimited list of all of the headers that are used and should be made available to scripts running in the browser.

Asp.net can do this with a Cors policy pretty easily.

I don't have a PR for this but I'm trying to get your opinion if something like this could be included under the veil of Htmx.Net.

Cors policy building could be made available via an extensions method (HtmxCorsPolicyBuilderExtensions.cs?) that extends the CorsPolicyBuilder?

Effectively add a method kind of like this (this is coded in browser so I'm spitballing):

    /// <summary>
    /// Adds Htmx request headers to the policy.
    /// </summary>
    /// <returns>The current policy builder.</returns>
    public static CorsPolicyBuilder WithHtmxHeaders(this CorsPolicyBuilder policyBuilder)
    {
        policyBuilder.WithHeaders( ... string array of htmx request headers ...);

        return policyBuilder;
    }
    /// <summary>
    /// Adds Htmx response headers to the policy.
    /// </summary>
    /// <returns>The current policy builder.</returns>
    public static CorsPolicyBuilder WithExposedHtmxHeaders(this CorsPolicyBuilder policyBuilder)
    {
        policyBuilder.WithExposedHeaders( ... string array of htmx response headers ...);

        return policyBuilder;
    }

Then build your policy with something like:

var  MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      policy  =>
                      {
                          policy.WithOrigins("http://example.com", "http://www.contoso.com")
                                   .WithHtmxHeaders()  // Add request headers
                                   .WithExposedHtmxHeaders()  // Add response headers
                      });
});

Thoughts?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.