Code Monkey home page Code Monkey logo

htmxor's Introduction

Htmxor - supercharging Blazor Static SSR with Htmx

Htmxor logo

This packages enables Blazor Static SSR (.NET 8 and later) to be used seamlessly with Htmx.

Blazor Static SSR comes with basic interactivity via enhanced navigation and enhanced form handling. Adding Htmx (htmx.org) to the mix gives you access to another level of interactivity while still retaining all the advantages of Blazor SSR stateless nature.

Nuget: https://www.nuget.org/packages/Htmxor

Documentation

See https://github.com/egil/Htmxor/blob/main/docs/index.md.

Samples

The following Blazor Web Apps (Htmxor) are used to test Htmxor and demo the capabilities of it.

htmxor's People

Contributors

asaf92 avatar egil avatar stefh 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

htmxor's Issues

HtmxContext request/response scoped type

Provide a HtmxContext type that leverages the HttpContext to provide easy access to Htmx specific request headers and setting Htmx specific response headers and status codes.

The idea is to create a HtmxContext type that exposes HtmxRequest Request { get; } and HtmxResponse { get; } properties, which mirrors the HttpContext, HttpRequest and HttpReponse types users are already familiar with from asp.net core, i.e.:

public class HtmxContext(HttpContext context)
{
  public HtmxRequest Request { get; }
  public HtmxResponse Response { get; }
}

The implementation of HtmxRequest would just contain a bunch of properties that matches the names of the Htmx headers and make them available as the most relevant .NET types.

For HtmxResponse, besides having writable property setters, there may be additional methods that make sending Htmx headers correctly easier, if needed.


  • getting request headers
  • setting response headers

Naming convention for methods, types, components.

Right now the library uses a mix of different ways to name things. I prefer consistency :)

Rules:

  • Htmxor-specific types, e.g. HtmxorNavigationManager, SHOULD be prefixed with Htmxor (or IHtmxor), if they are public.
    • Internal types should only be prefixed with Htmxor if their name would otherwise conflict with a type in Blazor/aspnetcore.
  • Htmx-specific types, e.g. HtmxConfig, SHOULD be prefixed with Htmx.
  • All components or types used in/by users in components should be prefixed with Htmx, e.g. HtmxRoute.
  • All attributes including custom attributes should use the prefix hx like Htmx do. E.g. @hxget.

This does mean the Hx prefix goes away, such that e.g. HxRoute becomes HtmxRoute.

Input is welcome.

HtmxRazorComponentEndpointInvoker - custom endpoint invoker

There may be a need for a custom RazorComponentEndpointInvoker which supports requests initiated by Htmx.

Whatever type is registered as the IRazorComponentEndpointInvoker in the app is used to invoke Blazor endpoints by RazorComponentEndpointFactory. If that endpoint Htmxor keeps using the builtin RazorComponentEndpointFactory a custom IRazorComponentEndpointInvoker should probably get the other registered RazorComponentEndpointInvoker and call that when a request is a non HX request.

Proper Usage of `<HtmxPartial>`

Hello @egil! I love what you're doing here! I have no doubt this will be an amazing contribution to the Blazor ecosystem!

I was trying to learn the ideas/concepts behind this library and even though I have started utilizing htmx in a few of my projects, reading through your code samples was the first time I had run across the idea of template-fragments. I love it! that's so much more concise than having to deal with directories of component partials.

That being said, I noticed some weird behavior regarding your <HtmxPartial> component. Riffing off of your ./samples/MinimalHtmxorApp/Components/Pages/Counter.razor component, I was playing with this test component to try to see how you would use multiple <HtmxPartial>'s in a single component:

@page "/counter"
@using Htmxor
<PageTitle>Counter</PageTitle>

<h1>Counter</h1>
<p>
    This is a htmx enabled counter. It uses the <code>&lt;HtmxPartial&gt;</code> to
    enable the concept <a href="https://htmx.org/essays/template-fragments/" target="_blank">template fragments</a>.
</p>
<p>
    When Htmxor sees a <code>&lt;HtmxPartial&gt;</code> component, it will only render it's content, and not the surrounding
    content, like these paragraph elements.
</p>

<div hx-target="this" id="counter">
    <HtmxPartial>
        <p role="status">
            Current count: @CurrentCount
        </p>
        <button class="btn btn-primary"
                hx-put="/counter"
                hx-vals='{ "Inc": true, "CurrentCount": @(CurrentCount) }'
                @onput="UpdateCount">
            Click to increment
            
        </button>
    </HtmxPartial>
</div>
<div hx-target="this" id="test">
    <HtmxPartial>
        <div hx-get="/counter?SayHello=true&ReloadCount=0" hx-swap="outerHTML" hx-trigger="load delay:5s">
            @if (SayHello)
            {
                <p>Hello!!</p>
                <p>@ReloadCount</p>
            }
            else
            {
                <p>Loading...</p>
            }
        </div>
    </HtmxPartial>
</div>

@code {
    [SupplyParameterFromQuery] private bool SayHello { get; set; }
    [SupplyParameterFromQuery] private int ReloadCount { get; set; } = -1;
    [SupplyParameterFromForm] private bool Inc { get; set; }
    [SupplyParameterFromForm] private int CurrentCount { get; set; } = 0;

    protected override void OnInitialized()
    {
        ReloadCount++;
    }

    private void UpdateCount(HtmxEventArgs args)
    {
        // Access the HtmxContextEventArgs to control
        // the response headers to the client.
        // E.g.: args.Response.StatusCode(201);

        CurrentCount = Inc ? CurrentCount + 1 : CurrentCount - 1;
    }
}


Looking at the code, I was expecting that on a counter button click, I would get a response from the server that patched simply the div#counter element (which is exactly what happens), and after 5 seconds when the div#test triggers, it would be patched with the contents of the second <HtmxPartial>. However, it actually gets patched with the content of the counter partial:

image

image

Am I misunderstanding how the <HtmxPartial> component is supposed to work? Is it not intended to be used multiple times within a single component? Is there some way of naming/identifying the individual instances such that the Renderer can tell the difference when patching the DOM?

Thanks again for your work on this! I'm excited to follow your progress!

Beta 1 release todo

  • #45
  • Don't require HxRoute the attribute if the component already has the route attribute.
  • Ensure validation errors from forms are supported via the standard Blazor form components.
  • #5
  • Ensure that multiple HX partial components can exist in the same render tree together, and only the ones are rendered during an HX request.
  • #46
  • #25

Track htmx v2 - htmxor should support v2

While there is only an alpha release of htmx, it makes sense that htmxor supports it. Depending on htmx release cadance, it may even be the default.

https://v2-0v2-0.htmx.org/migration-guide-htmx-1/


These are changes that likely impact htmxor directly:

  • htmx now includes head tag merging functionality out of the box
  • Attribute inheritance can now be disabled entirely via the htmx.config.disableInheritance config variable
  • Response code handling is now configurable via the htmx.config.responseHandling config variable
  • DELETE requests now use parameters, rather than form encoded bodies, for their payload (This is in accordance w/ the spec.)

"Template fragment" component in Htmxor

[UPDATED: combined a bunch of comments below into a general description/issue].

Htmxor needs a good way to do "template fragments". The library currently have a very rough first attempt, <HtmxPartial>. It may need a better name and certainly more features.

Let me start by explaining how <HtmxPartial> works right now.

Lets use this as the example:

@page "/counter"
<PageTitle>Counter</PageTitle>
<div id="counter">
    <HtmxPartial>
        <p role="status">
            Current count: @CurrentCount
        </p>
        <button class="btn btn-primary"
                hx-put="/counter"
                hx-vals='{ "CurrentCount": @(CurrentCount) }'
                hx-target="#counter"
                @onput="IncrementCount">
            Click me
        </button>
    </HtmxPartial>
</div>

@code {
    [SupplyParameterFromForm]
    private int CurrentCount { get; set; } = 0;

    private void IncrementCount(HtmxContextEventArgs args)
    {
        CurrentCount++;
    }
}

Normal request or hx-boosted request

During a normal request or hx-boosted request, the full component tree is rendered, i.e. starting with App.razor, all the way down to the Counter.razor page.

Here the full component tree is rendered out, including <HtmxPartial> and its child content.

hx-request to /counter

When a hx-request is received that targets the component, that component's component-tree is searched, and since a component is found whose [Parameter] public bool Condition { get; set; } = true; returns true, only that <HtmxPartial> component and its children is rendered.

Normal request or htmx request

During rendering, if a component is reached, its Condition parameter is checked, and if it returns false, the component and its children is not rendered.

This happens during both normal and htmx requests.

A more detailed discussion of the proposed improvements is in the comment #25 (comment) below.

Cross site request forgery - where should the token be placed?

While investigating #50, I discovered that (output) caching will not be enabled if cookies are set.

This is relevant because current Htmxor embeds the CSRF token in a cookie, which htmxor.js then attaches to HX requests, if a CSRF token is not already embedded in the form data.

However, this is probably not the right approach long term, it does influence caching, and may not be the correct approach from a security perspective.

The current model Blazor follows is that forms should include a hidden input field via the <AntiforgeryToken /> component, or by using the <EditForm> component. Each time a form is rendered, Blazor will update the token, even though it can be reused.

We could simply switch to a model where users explicitly have to include the csrf token manually as needed in, but that is tedious at best.

Here are the options I see:

  1. Have the renderer automatically add a hxor-csrf-token="xxxx" attribute on every element that has a hx-post, hx-put, hx-patch, hx-delete attribute. This adds quite a lot of noise to the markup, especially since the token is 60+ chars long.

  2. A variation of this would be to only add the hxor-csrf-token attribute on the top level element being rendered, and let it be inherited down to any child elements.

In both cases, htmxor.js will intercept calls and ensure that the tokens value is correctly included in requests.

  1. Have a <meta id="hxor-csrf-token" name="hxor-csrf-token" value="XXX"> element that gets updated using OOB during each request that includes element a hx-post, hx-put, hx-patch, hx-delete attribute. This would add a minimum noise the markup of the page, but will instead add that noise in the request response.

Neither of the solutions can depend on htmxor intercepting the response from the server and string the token, since it may not be htmx that initiated the request. That was also the reason I decided to use cookies in my first attempt. All three solutions mentioned above would work with both normal and direct requests, but adding a custom header that htmx would have to extract and store somewhere in the DOM would not.

Control any response headers from a component

Htmxor should include a generic way for developers to declaratively influence any headers during when returning a response to a request, not just the htmx specific headers that are exposed via the HtmxContext.Response, aka. HtmxResponse object.

There are two scenarios in play:

Headers that are dynamically set based on request or dynamic content:
Right now, developers can [Inject] private HttpContext Ctx in a component and set response headers through that. For convenience, this could also be enabled through a generic Headers property on the HtmxResponse type, which would just point to the HtmlContext.Response.Headers dictionary.

Headers that are statically determined at compile time:
These are good candidates for a declarative approach. My idea is for Htmxor to include a IControlResponseHeaders interface, that any attribute can implement. Then, developers can implement custom attributes that implement this interface and use these attributes in their components declaratively.

Here is the interface definition:

public interface IControlResponseHeaders
{
    void Apply(IHeaderDictionary headers);
}

And an example attribute:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class ContentLanguageAttribute(string language) : Attribute, IControlResponseHeaders
{
  public void Apply(IHeaderDictionary headers)
  {
    headers["Content-Language"] = language;
  }
}

This can then be used like this:

@attribute [ContentLanguage("da-DK")]
@page "/da/home"
...

Inspired by: https://mastodon.social/@khalidabuhakmeh/112339241113769565

UseEmbeddedHtmx?

The comment in HtmxHeadOutlet is as follows:

/// <summary>
/// Gets or sets whether or not to add the scripts thats that reference the embedded version of Htmx. Default is <see langword="true"/>.
/// </summary>
/// <remarks>
/// If this set to <see langword="false"/>, include the <c>_content/Htmxor/htmxor.js</c> your application manually.
/// </remarks>
[Parameter] public bool UseEmbeddedHtmx { get; set; } = true;

But when setting this to false, in addition to "include the _content/Htmxor/htmxor.js your application manually", I also need to reference the javascript file for htmx myself, correct?

(BTW I don't understand why I would need to include the _content/Htmxor/htmxor.js your application manually) ?

Antiforgery token integration

For components with @attribute [RequireAntiforgeryToken(required: true)], do the following:

  • An element has an hx- action attribute (except GET) NOT inside a form, add a hxor-aft attribute whose value is the antiforgery token to the element.
  • Update htmxor.js to look for an hxor-aft when triggered on an element.
  • An element has an hx- action attribute (except GET) inside a form, ensure the standard hidden input field is rendered to the dom with the antiforgery token before the end of the form, if not already provided.

Implement `HxRoute` aware endpoint discovery/mapping

A component can have either or both a [Route] and [HxRoute] attribute. A goal for Htmxor should be to change as little as possible of the normal behavior of Blazor, so whether to use the existing mapping and endpoint handling for components with only the [Route] attribute may be an option.

Currently, it seems components with [Route] attribute is discovered like this: https://source.dot.net/#Microsoft.AspNetCore.Components.Endpoints/Discovery/IRazorComponentApplication.cs,20

This is used from here: https://source.dot.net/#Microsoft.AspNetCore.Components.Endpoints/Builder/RazorComponentsEndpointRouteBuilderExtensions.cs,36

Questions:

  • Should Htmxor completely replace the existing discovery and mapping so it has complete control?
  • Or should it allow the existing MapRazorComponents calls to do their thing and perhaps use modify endpoint discovery and registrations via the RazorComponentsEndpointConventionBuilder which is returned from MapRazorComponents?

HtmxRequestView component

The idea is to provide a component similar to AuthorizeView that allows the user to render different content in a declarative way depending on whether a request is from HTMX or not.

Suggestion:

<HtmxRequestView>
  <FullPageContent>
    This only renders if this is not a htmx request.

    It can include the HtmxContent by referencing @context.HtmxContent
  </FullPageContent>
  <HtmxContent>
    This only renders when the request is made by Htmx
  </HtmxContent>
</HtmxRequestView>

Handle hx-boost requests according to hx-target

          What is the mechanism for targeting that HtmxPartial component?

Normal request or hx-boosted request

During a normal request or hx-boosted request, the full component tree is rendered, i.e. starting with App.razor, all the way down to the Counter.razor page.

This is potentially incorrect. On an hx-boosted request the rendered output can still target a particular selector. The documentation doesn't give you much to indicate it but hx-target is still adhered to along with hx-swap as well. Even if hx-target is body then hx-boosted requests only replace the body of the current page without changing the url (used for non-GET requests). If hx-target is "window" or empty (?) then it's a full page request.

For example:

<navbar>
    <a href="/home" hx-swap="innerHTML transition:true scroll:main:top show:none" hx-boost="true" hx-target="#main_content">Home</a>
</navbar>

<main>
    <div id="main_content"></div>
</main>

This would load the content at url /home as boosted, swapping it into #main_content with a transition, immediately setting the scrollbar to the top of main without any animation, and overriding any default show scroll animations. This would allow you to load content into a particular div just as you would with hx-get.

As such, a full page request to /home should render normally and a boosted page request to /home is the same as any other hx-get.

I see you added something more so I'm going to read that before adding anything further.

Originally posted by @tanczosm in #25 (comment)

Map from NavigationManager to HX-headers

How should we map between the NavigationManager.NavigateTo calls with and without NavigationOptions and HX- headers?

My current thinking is:

Uri kind ForceLoad ReplaceHistoryEntry HX headers
relative uri false false HX-Location: relative uri
relative uri true false HX-Redirect: relative uri
relative uri false true HX-Replace-Url: relative uri
relative uri true true HX-Redirect: relative uri, HX-Replace-Url: relative uri
another domain uri true/false true/false HX-Redirect: another domain uri (assume implicit HX-Push-Url)

If the NavigateTo call happens during a non-hx request, a standard 302 redirect status code is used with the absolute url.

Ensure proper handling of hx attributes

If the value in hx attributes contains certain characters used in javascript, these are escaped by default by the renderer. That breaks functionality.

  • Enable easy serialization of object into hx-header and hx-vals attributes, such that the JSON is correctly embedded in the markup.
  • Ensure hx attributes values are escaped as expected.

Attributes that contains JSON or JavaScript

How to associate event handler callback

When a component has markup that includes event handlers, Htmxor needs to be able unambiguously identify which event handler to invoke., e.g.:

These two are unambiguous because they reference the 
same event handler method even though their hx-get attribute 
has the same value.
<div hx-get="/" @hxget=@HandleGet />
<div hx-get="/" @hxget=@HandleGet />

These two are ambiguous because the event handlers (lambda) 
are distinct and they share the same hx-get value.
Will cause Htmxor to throw.
<div hx-get="/page" @hxget=(() => {}) />
<div hx-get="/page" @hxget=(() => {}) />

These two are unambiguous because they reference 
distinct event handlers and their hx-get attribute are different.
<div hx-get="/page2" @hxget=(() => {}) />
<div hx-get="/page3" @hxget=(() => {}) />

@code {
  private void HandleGet(HtmxEventArgs args) { }
}

This works as illustrated above, i.e. based on the hx-ACTION's attribute value. However, this could be extended to include other unique attributes on the element the handler is attached to, e.g.:

  • ID of the element. These are expected to be unique.
  • Value passed to @key Blazor attribute. This is expected to be unique.
  • A fallback, e.g. custom hx-ACTION-key attribute.

This avoids users having to custom query parameter on their action URLs.

Update renderer to correctly convert SwapStyle enum

Originally posted by @egil in #49 (comment)

The enum option has the advantage of not being "stringly-typed", but will require a small modification to the renderer such that if it is used directly in an attribute, it produces the correct markup.

E.g.: [email protected] renders as hx-swap="beforebegin".

That is a simple addition I can make to the renderer.

The SwapStyle enum can also be used as a starting point for the builder, which it already is, such that users can start with the enum and "pay the cost" of using the builder if they need the additional help.

I am in favor of changing the constants we that overlap with enums to internal, to make the API simpler for the end user.

Antiforgerytoken support for none-form requests

Enable cross-site scripting protection for HTMX page loads without forms with hidden anti-forgery tokens.

The current implementation ensures a cookie that HTMX can read is always sent on each request. This enables it to pick that up if the request does not already contain the anti-forgery token.

Cookies carry an overhead. An alternative would be to send the token in response headers for Htmx-initiated requests. Then htmx can pull the token from the response. For first-time requests or full-page requests, the token could also be included in the Htmx config written to the head element.

Inspiration:

Make <PageTitle> component HX-request aware

By default, htmx will update the page's title if it finds a <title> tag in the response content.

Since the <PageTitle> component is used in Blazor to do the same thing from any page component, we can make the integration work seamlessly by overriding the <PageTitle> component behavior to write out a <title> element where it is being added to the component tree, allowing HX to do its magic.

That should only happen when Htmxor is processing an HX request. On normal full page loads the <PageTitle> component should behave as normal.

This can be done by Htmxor including a custom IComponentActivator that returns a different component type during HX requests.

Hx endpoints and custom pipeline

Replace or extend all aspects of the default Blazor SSR endpoints router, renderer and services.

It seems that the Blazor in .NET 8 leverages asp.net cores existing Endpoint discovery and registration base tyes/primitives, so while the Blazor-specific one is not documented outside of the source code, there should be info generally available about Endpoint discovery and registration in general.

Routing

When multiple pages/components have routes that match a request, the most specific match should be used. The HxRoute attributes allow the user to specify HX headers that should match a specific value. If these are used, the route precedes the regular Route attribute.

Enable Htmxor and standard Blazor to co-exsist

Ideally, users should be able to add Htmxor to their Blazor apps, and start taking advantage of it in some parts of the solution, and keep using other render modes in other parts.

There is a few big questions that needs answering to understand if this is possible:

  1. Can htmx and blazor.web.js coexist without treading on each other's feet?

  2. Can we choose the rendering pipeline per request - basically implement a request handler that forwards to either the native endpoint handler or htmxor's.

  3. Will that also enable the switching between the three render modes Blazor 8 comes with out of the box?

  4. Do we need to explicitly introduce a custom render mode, to ensure that Blazor's standard rendering logic does not attempt to render components that leverage htmx and htmxor or is that unnecessary?

Resources:

@khalidabuhakmeh's article hints of co-exsistance being possible: https://khalidabuhakmeh.com/how-to-use-blazor-server-rendered-components-with-htmx

Inspired by the discussion #42.

SwapStyle enum serialization is incorrect

The current serializer for SwapStyle uses JsonNamingPolicy.CamelCase, which converts SwapStyle.AfterBegin to "afterBegin" instead of the correct "afterbegin". This issue impacts several enum values.

Feature Proposal: Introduce HtmxSwapService for Out-of-Band Swapping

Description:

Service Name: HtmxSwapService (Alternative names: OOBSwapService, ComponentSwapService)

Currently, there's a need for a mechanism to perform Out-of-Band (OOB) swaps in response to htmx requests. This feature request proposes the addition of a scoped service named HtmxSwapService to fulfill this requirement. While it is possible to add OOB swaps anywhere you want, they still need to be largely done on HTMX requests rather than full page renders.

A service would then allow you to easily inject the components / html you want to render on an out-of-band basis. It could also be utilized by third-party components as well to add interactivity to the page.

Htmx supports the following swaps:

Functionality:

  • Provides a method to add razor components with a parameter dictionary/object or accepts RenderFragment as a parameter.
  • Components to render/renderfragments are stored to a list
  • Renders the fragments at the end of the HTTP response specifically in response to htmx requests. Can be done at the very end of the request, after the closing body tag.

Example Usage:

  • Inject an updated navbar that gets rendered with the response on an HTMX request
  • Inject flash/toast/notification messages that appear on screen in response to an error, warning, or other message
  • Update header tags to reflect the current page state
  • Inject any new content that can piggyback on top of any other htmx request (for example update a notifications counter for the user)

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.