feat: htmx powered requests, improved loading indicator

feat: loading indicator and htmx form submit
This commit is contained in:
Björn Fromme
2026-03-16 11:59:10 +01:00
parent 023ecdebc9
commit 83589ee677
20 changed files with 658 additions and 197 deletions
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Htmx;
use Symfony\Component\HttpFoundation\Response;
class HxRedirectResponse extends Response
{
/**
* Creates an HTMX redirect response.
*
* Constructs a response that instructs the HTMX client to navigate to the
* specified URL. The response includes the HX-Redirect header with the target
* URL. Optionally includes HX-Retarget header if a different target element
* is specified for the response content.
*
* @param string $url The URL to redirect to
* @param string|null $retarget Optional CSS selector for the target element
*/
public function __construct(string $url, ?string $retarget = null)
{
$headers = [
'HX-Redirect' => $url,
];
if (null !== $retarget) {
$headers['HX-Retarget'] = $retarget;
}
return parent::__construct(null, Response::HTTP_OK, $headers);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Htmx;
use Symfony\Component\HttpFoundation\Response;
class HxRefreshResponse extends Response
{
/**
* Creates an HTMX refresh response.
*
* Constructs a response that instructs the HTMX client to perform a full page
* refresh. The response includes the HX-Refresh header set to 'true', which
* signals the HTMX client to reload the entire page. This response typically
* contains no content since the page will be completely reloaded.
*/
public function __construct()
{
return parent::__construct(null, Response::HTTP_OK, ['HX-Refresh' => 'true']);
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Htmx;
use Symfony\Component\HttpFoundation\Response;
class HxStopPollingResponse extends Response
{
/**
* Creates an HTMX stop polling response.
*
* Constructs a response that instructs the HTMX client to stop polling. The response
* includes the specified content and uses HTTP status code 286, which is recognized
* by HTMX as a signal to terminate polling. This allows the server to control
* client-side polling behavior and prevent unnecessary network requests.
*
* @param string $content The response content to send to the client
*/
public function __construct(string $content)
{
return parent::__construct($content, 286);
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
namespace App\Htmx;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Trait providing HTMX-specific functionality for controllers.
*
* This trait provides methods to handle HTMX requests and responses, including
* conditional rendering based on HTMX headers, HTMX-specific redirects, and
* multi-block rendering capabilities. It simplifies the integration of HTMX
* functionality into Symfony controllers by providing common patterns for
* detecting HTMX requests and generating appropriate responses.
*/
trait HxTrait
{
/**
* Determines if the current request is an HTMX request.
*
* Checks the HX-Request header to identify if the request was made via HTMX.
* This allows controllers to provide different responses for HTMX vs regular
* HTTP requests, enabling progressive enhancement patterns.
*
* @param Request $request The current HTTP request
*
* @return bool True if the request is an HTMX request, false otherwise
*/
public function isHxRequest(Request $request): bool
{
return 'true' === $request->headers->get('HX-Request');
}
/**
* Renders a template with HTMX-aware block selection.
*
* Renders either a specific template block or the full template based on
* whether the request is an HTMX request. When an HTMX request is detected
* and a block is specified, only that block is rendered. Otherwise, the
* full template is rendered. This enables efficient partial page updates
* for HTMX requests while maintaining full page rendering for regular requests.
*
* @param Request $request The current HTTP request
* @param string $template The template name to render
* @param array $parameters Template parameters to pass to the view
* @param string|null $block The specific block to render for HTMX requests
*
* @return Response The rendered response
*/
public function hxRender(
Request $request,
string $template,
array $parameters = [],
?string $block = null,
): Response {
if (null !== $block && true === $this->isHxRequest($request)) {
return $this->renderBlock($template, $block, $parameters);
}
return $this->render($template, $parameters);
}
/**
* Performs an HTMX-aware redirect.
*
* Returns an HTMX-specific redirect response when the request is an HTMX
* request, or a standard redirect response for regular HTTP requests.
* HTMX redirects are handled differently by the client, allowing for
* smoother user experiences in single-page application contexts.
*
* @param Request $request The current HTTP request
* @param string $url The URL to redirect to
*
* @return Response Either an HxRedirectResponse or RedirectResponse
*/
public function hxRedirect(Request $request, string $url): Response
{
if (true === $this->isHxRequest($request)) {
return new HxRedirectResponse($url);
}
return new RedirectResponse($url);
}
/**
* Renders multiple template blocks in a single response.
*
* Combines multiple template blocks into a single response content. This is
* useful for HTMX requests that need to update multiple parts of the page
* simultaneously. Each block in the array should contain 'template', 'block',
* and 'parameters' keys to define what to render.
*
* @param array $blocks Array of block definitions, each containing:
* - 'template': The template name
* - 'block': The block name to render
* - 'parameters': Template parameters
*
* @return Response Response containing all rendered blocks concatenated
*/
public function hxRenderBlocks(array $blocks): Response
{
$content = '';
foreach ($blocks as $block) {
$content .= $this->renderBlockView($block['template'], $block['block'], $block['parameters']);
}
return new Response($content);
}
/**
* Renders multiple Twig blocks for an HTMX Out-of-Band swap response.
*
* This method is specifically designed for HTMX OOB (Out-of-Band) swaps,
* where multiple blocks need to be updated simultaneously. It adds an
* `htmx_oob_swap` flag to the template context, allowing templates to
* conditionally add the hx-swap-oob attribute to elements.
*
* @param string $templateName The name of the Twig template
* @param string[] $blockNames An array of block names to render
* @param array<string, mixed> $context The context to pass to the template
*
* @return Response Response containing all rendered blocks with OOB context
*/
protected function htmxOobResponse(string $templateName, array $blockNames, array $context = []): Response
{
$html = '';
// Add a flag to the context so templates can conditionally add the hx-swap-oob attribute.
$oobContext = $context + ['htmx_oob_swap' => true];
foreach ($blockNames as $blockName) {
// Use renderBlockView() to get the raw HTML string for each block.
$html .= $this->renderBlockView($templateName, $blockName, $oobContext);
}
return new Response($html);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Htmx;
use Symfony\Component\HttpFoundation\Response;
class HxTriggerResponse extends Response
{
/**
* Creates an HTMX trigger response with custom event triggering capabilities.
*
* Constructs a response that will trigger a custom JavaScript event on the client
* when the HTMX request completes. The trigger can be a simple event name or a
* JSON object for more complex event data. Optionally disables content swapping
* to prevent the response content from being inserted into the DOM.
*
* @param string $content The response content (typically empty for trigger-only responses)
* @param string $trigger The event name or JSON object to trigger on the client
* @param bool $disableSwap Whether to disable HTMX content swapping (default: true)
*/
public function __construct(string $content, string $trigger, bool $disableSwap = true)
{
$headers = ['HX-Trigger' => $trigger];
if (true === $disableSwap) {
$headers['HX-Reswap'] = 'none';
}
return parent::__construct($content, Response::HTTP_OK, $headers);
}
}