volfpeter / fasthx

FastAPI server-side rendering with built-in HTMX support.
https://volfpeter.github.io/fasthx/
MIT License
458 stars 9 forks source link

Built-in option to render full page, fragments or JSON #35

Closed dries007 closed 5 days ago

dries007 commented 1 month ago

I would like to be able to use a single annotation for the following setup:

  1. A request with Accept header set to JSON: The application returns a JSON responce.
  2. A request from a HTMX: The application returns a fragment only (jinja2-fragments comes to mind). Preferably with the name of the block being easy to set dynamically.
  3. Any other request: The entire template gets rendered, based on the default template.

This, in combination with json-enc, progressive enhancement (hx-boost) and the already existing template header would make it quite easy to develop an HTML first, but API capable application.

I think this is already possible via either an additional annotation or extra boiler plate, but I would like to integrate it into fasthx proper. Since this would change behaviour I suggest it should be a new annotation. I'm going to do some experimenting and may make a MR later, but it might take a while, so I welcome possible feedback on the idea already.

EDIT:

Just realized: A smaller version of this would be to have the accept header based JSON in combination with the template header and a good default. This could already solve part of the issue, but would lead to many duplicate template files (1 "page" and 1 "default" which extends a base template and includes the "page" template).

volfpeter commented 1 month ago

hx() already returns JSON if the current request doesn't have an HX-Request="true" header. So if I understand correctly, basically the missing thing is Jinja fragments support, right? I'm not against that idea at all, but the feature should be added in a way that doesn't affect the current features and users (and also doesn't add a mandatory new dependency).

I haven't used Jinja fragments yet, but I guess all that'd need to change is the Jinja_make_response() method, as that's doing the template rendering. We could for example consider adding a JinjaWithFragments subclass that overrides this method and adds the necessary feature.

In any case, the solution must work correctly with Starlette's Jinja context processors and I'll also need to see how widely used Jinja fragments are. Depending on these, if the necessary feature can be added by subclassing Jinja, then maybe it'd be best to have this extra feature in a fasthx-jinja-fragments library. We'll see if there's a working proof of concept.

dries007 commented 1 month ago

So I think the fragments can be solved with some subclassing and maybe clever use of the template header system, but the thing that's missing (at least without adding boilerplate to every route) is to only output JSON if the Accept header is application/json and fall back to HTML by default.

In effect the inverse of what the hx decorator does.

volfpeter commented 1 month ago

Okay, I see. So you basically need the page() decorator with as escape hatch to skip rendering under certain circumstances.

You have of course other options, but I wouldn't be against adding a new, optional argument to page(), (e.g. a Callable[[Request], bool] | None = None) that expects a function that returns if rendering should be skipped. A bit similar to hx's no_data argument.

What do you think?

dries007 commented 1 month ago

That sounds like a better idea than what I had in mind, especially with some additional helpers. I'm going to wrap up my "prototype" and push it to a branch so we can compare notes.

dries007 commented 1 month ago

So I've made a an experimental project so I can share without leaking things I can't share: https://github.com/dries007/xjf

dries007 commented 5 days ago

As discussed elsewhere, this ends up being easier (and better) solved with lazy loading and HTMX on the client site.