-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HTMX integration #41
Comments
As an alternative to using <body {{#with request.values.token}}hx-headers='{"X-CSRF-Token": "{{.}}"}'{{/with}}>
<!-- ... -->
</body> See https://www.mattlayman.com/blog/2021/how-to-htmx-django/ and https://django-htmx.readthedocs.io/en/latest/tips.html |
See also https://laravel.io/articles/getting-started-with-htmx-in-laravel-an-overview and https://github.com/mauricius/laravel-htmx. The article https://dev.to/turculaurentiu91/laravel-htmx--g0n features progressive enhanchement techniques: Ensure that HTMX requests to the edit chirp endpoint receive only the edit chirp component, while other requests receive the whole page, aligning with the "progressive enhancement" pattern. public function edit(Request $request, Chirp $chirp): Response
{
$this->authorize('update', $chirp);
if ($request->header('HX-Request')) {
return response()->view('components.chirps.edit', [
'chirp' => $chirp,
]);
}
return response()->view('chirps.edit', [
'chirp' => $chirp,
]);
} |
To use the bundling mechansim, put the following inside the file package.json: {
"dependencies": {
"htmx.org": "^1.9"
},
"bundles": {
"vendor": {"htmx.org": ["dist/htmx.min.js"]}
}
} ...and then run |
CSRF Token header was released in https://github.com/xp-forge/frontend/releases/tag/v5.3.0 |
To realize this with the frontend library, we specify a full form including the <form name="post" hx-post="/messages" action="/messages" method="POST">
<input type="hidden" name="token" value="{{request.values.token}}">
<input type="text" name="message" placeholder="Your message...">
<button type="submit">Post</button>
</form> ...and then declare the handler as follows: #[Handler('/messages')]
class Messages {
private $posts= [];
#[Get]
public function list() {
return View::named('reactive')->with(['posts' => $this->posts]);
}
#[Post]
public function create(#[Value] $user, #[Param] $message, #[Header('Hx-Request')] $hx= false) {
$this->posts[]= ['message' => $message, 'author' => $user['name'], 'date' => date('Y-m-d H:i')];
if ($hx) {
return View::named('reactive')->fragment('list')->with(['posts' => $this->posts]);
} else {
return View::redirect('/');
}
}
} Should JavaScript be disabled or error on loading, the user could still use the page, with the user being redirected back after their POST request has invoked the create handler. To make this work for HTTP method such as The above could be generalized by the following: Matching up target and fragment:<div class="segments" hx-target="#list"> <!-- The target here (w/o the leading "#")... -->
<div id="list" class="segments"> <!-- ...equals this ID -->
{{#*fragment "list"}} <!-- ...and this fragment's name -->
{{#each posts}}
...
{{/each}}
{{/fragment}}
</div>
<form name="post" hx-post="/messages">
<!-- Create post form -->
</form>
</div> Creating this small integration class:class Htmx {
public function rerender($req, $view) {
if ('true' === $req->header('Hx-Request')) {
return $view()->fragment($req->header('Hx-Fragment') ?? $req->header('Hx-Target'));
} else {
return View::redirect('/');
}
}
} Note: If we cannot use an ID, we can pass the header with the fragment's name via Using this Htmx class inside the Messages handler as follows: #[Handler('/messages')]
class Messages {
private $posts= [];
+ private $htmx;
+ public function __construct() {
+ $this->htmx= new Htmx();
+ }
#[Get]
public function list() {
return View::named('reactive')->with(['posts' => $this->posts]);
}
#[Post]
- public function create(#[Value] $user, #[Param] $message, #[Header('Hx-Request')] $hx= false) {
+ public function create(#[Value] $user, #[Param] $message, #[Request] $request) {
$this->posts[]= ['message' => $message, 'author' => $user['name'], 'date' => date('Y-m-d H:i')];
-
- if ($hx) {
- return View::named('reactive')->fragment('list')->with(['posts' => $this->posts]);
- } else {
- return View::redirect('/');
- }
+ return $this->htmx->rerender($req, $this->list(...));
}
} |
A good example usecase is shown in https://www.youtube.com/watch?v=akd7u69k27k - implemented #44 to short-hand setting template and fragment, and #45 to support the In the PHP implementation, I chose to use BeforeThis still has some minimal JS involved, and adds some HTMX-specific code to the PHP part: <button hx-delete="/todos/{{id}}" hx-on:delete-todo="this.closest('tr').remove()">Delete</button> #[Delete('/todos/{id}')]
public function remove($id) {
$this->conn->delete('from todo where id = %d', $id);
return View::empty()->status(204)->header('HX-Trigger', 'delete-todo');
} This could be something like AfterThis makes it necessary to a non-204 status code - otherwise, HTMX will not do anything. <button hx-delete="/todos/{{id}}" hx-target="closest tr" hx-swap="delete">Delete</button> #[Delete('/todos/{id}')]
public function remove($id) {
$this->conn->delete('from todo where id = %d', $id);
return View::empty()->status(202);
} This could also just use |
This does not work out of the box with AWS lambda & CloudFront - most probably needing an extra header configuration! |
How this is done with other languages & frameworks: https://htmx.org/server-examples/#php - once https://github.com/thekid/crews reaches an MVP state, we could add it there! |
The authentication part is now taken care of by https://github.com/xp-forge/htmx |
Motivation
HTMX is growing in popularity, and is a perfect fit for making server-rendered apps like those created with this library feel "reactive".
Source: https://trends.google.de/trends/explore?q=htmx&date=today%205-y#TIMESERIES
Given that, this issue explores what is necessary to integrate it well.
The basics
To use this library, simply add a script tag:
Note: You should be bundling your dependencies when going into production - see below
Now, we can make use of the
hx-*
attributes:Handling errors
When an HTMX request cannot reach the server (flaky wifi, offline, ...), there is no immediate feedback to the user except for in the browser console. This can be solved by adding an event listener as follows:
For requests causing internal server errors, we should also handle htmx:responseErrors; a complete implementation would include handling htmx:timeouts, e.g. by asking the user whether to continue.
See https://htmx.org/events/#htmx:sendError, https://htmx.org/events/#htmx:responseError and https://htmx.org/events/#htmx:timeout
CSRF token
This library includes automatic CSRF token handling. To include the CSRF token in HTMX requests, the following can be done:
Note: Support was added in PR #40
This will save adding the CSRF token manually to each element, e.g.
<span hx-post="/clicked" #{{if request.values.token}}hx-vals='{"token":"{{request.values.token}}"}'{{/if}}>...</span>
.Authentication
When an HTMX request triggers re-authentication, e.g. because the session expired, it might cause redirects to the HTMX target (
/clicked
in the above example) when using protocols such as OAuth. This is not desireable, we instead want to show an error message to the user that his or her session has expired. Solving this consists of raising an error in the backend:...and handling that in the frontend:
We could think about using a dedicated response codes to disambiguate, e.g. https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/421 ("Misdirected Request [...] indicates that the request was directed to a server that is not able to produce a response")
The text was updated successfully, but these errors were encountered: