diff --git a/.gitignore b/.gitignore index 244d127..77c68a6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /composer.phar /phpunit.xml /.phpunit.result.cache +/.phpunit.cache /phpunit.phar /config/Migrations/schema-dump-default.lock /vendor/ diff --git a/src/Controller/Component/HtmxComponent.php b/src/Controller/Component/HtmxComponent.php new file mode 100644 index 0000000..6bd0074 --- /dev/null +++ b/src/Controller/Component/HtmxComponent.php @@ -0,0 +1,130 @@ + + */ + protected array $_defaultConfig = []; + + /** + * @var \Cake\Controller\Controller + */ + protected $controller; + + /** + * @param array $config + * @return void + */ + public function initialize(array $config): void { + parent::initialize($config); + + $this->controller = $this->getController(); + } + + /** + * Convenience method to check if request is from HTMX + * + * @return bool + */ + public function isHtmx() + { + return $this->controller->getRequest()->getHeaderLine('HX-Request') === 'true'; + } + + /** + * Convenience method to check if request is Boosted + * + * @return bool + */ + public function isBoosted() + { + return $this->controller->getRequest()->getHeaderLine('HX-Boosted') === 'true'; + } + + /** + * Get HTMX target id + * + * @return string|null + */ + public function getHtmxTarget() + { + $target = $this->controller->getRequest()->getHeaderLine('HX-Target'); + + return $target ?: null; + } + + /** + * Get HTMX trigger id + * + * @return string|null + */ + public function getHtmxTrigger() + { + $trigger = $this->controller->getRequest()->getHeaderLine('HX-Trigger'); + + return $trigger ?: null; + } + + /** + * Get HTMX trigger name + * + * @return string|null + */ + public function getHtmxTriggerName() + { + $trigger = $this->controller->getRequest()->getHeaderLine('HX-Trigger-Name'); + + return $trigger ?: null; + } + + /** + * Set headers to cache this request. + * Opposite of Controller::disableCache() + * + * @param array|string $redirectTo + * @param bool $full if client side redirect should be a full page reload or not + * + * @return void + */ + public function clientSideRedirect(array|string $redirectTo, bool $full = false): void + { + $response = $this->controller->getResponse(); + if (is_array($redirectTo)) { + $redirectTo = Router::url($redirectTo); + } + $header = $full ? 'HX-Redirect' : 'HX-Location'; + $response = $response->withHeader($header, $redirectTo); + + $this->controller->setResponse($response); + } + + /** + * Set headers to cache this request. + * Opposite of Controller::disableCache() + * + * @param array|string $redirectTo + * @param bool $full if client side redirect should be a full page reload or not + * + * @return void + */ + public function clientSideRefresh(): void + { + $response = $this->controller->getResponse(); + $response = $response->withHeader('HX-Refresh', 'true'); + + $this->controller->setResponse($response); + } +} diff --git a/tests/TestCase/Controller/Component/HtmxComponentTest.php b/tests/TestCase/Controller/Component/HtmxComponentTest.php new file mode 100644 index 0000000..c121450 --- /dev/null +++ b/tests/TestCase/Controller/Component/HtmxComponentTest.php @@ -0,0 +1,201 @@ +controller = new Controller($request); + $registry = new ComponentRegistry($this->controller); + + $this->component = new HtmxComponent($registry); + + $this->routeBuilder = Router::createRouteBuilder('/'); + $this->routeBuilder->scope('/', function (RouteBuilder $routes) { + $routes->setRouteClass(DashedRoute::class); + $routes->get( + '/tests', + ['controller' => 'Tests', 'action' => 'index'] + ); + // ... + }); + } + + /** + * @return void + */ + public function tearDown(): void { + parent::tearDown(); + + unset($this->Controller); + } + + /** + * @return void + */ + public function testIsHtmx() { + $this->controller->setRequest($this->controller->getRequest()->withHeader('HX-Request', 'true')); + $this->assertTrue($this->component->isHtmx()); + + $this->controller->setRequest($this->controller->getRequest()->withHeader('hx-request', 'true')); + $this->assertTrue($this->component->isHtmx()); + } + + /** + * @return void + */ + public function testIsBoosted() { + $this->controller->setRequest($this->controller->getRequest()->withHeader('HX-Boosted', 'true')); + $this->assertTrue($this->component->isBoosted()); + + $this->controller->setRequest($this->controller->getRequest()->withHeader('hx-boosted', 'true')); + $this->assertTrue($this->component->isBoosted()); + } + + /** + * @return void + */ + public function testGetHtmxTarget() { + $this->controller->setRequest($this->controller->getRequest()->withHeader('HX-Target', 'test')); + $this->assertEquals('test', $this->component->getHtmxTarget()); + + $this->controller->setRequest($this->controller->getRequest()->withHeader('hx-target', 'TEST')); + $this->assertEquals('TEST', $this->component->getHtmxTarget()); + } + + /** + * @return void + */ + public function testGetHtmxTrigger() { + $this->controller->setRequest($this->controller->getRequest()->withHeader('HX-Trigger', 'test')); + $this->assertEquals('test', $this->component->getHtmxTrigger()); + + $this->controller->setRequest($this->controller->getRequest()->withHeader('hx-trigger', 'TEST')); + $this->assertEquals('TEST', $this->component->getHtmxTrigger()); + } + + /** + * @return void + */ + public function testGetHtmxTriggerName() { + $this->controller->setRequest($this->controller->getRequest()->withHeader('HX-Trigger-Name', 'test')); + $this->assertEquals('test', $this->component->getHtmxTriggerName()); + + $this->controller->setRequest($this->controller->getRequest()->withHeader('hx-trigger-name', 'TEST')); + $this->assertEquals('TEST', $this->component->getHtmxTriggerName()); + } + + /** + * @return void + */ + public function testClientSideRedirectString() + { + $response = $this->controller->getResponse(); + $this->assertEquals('', $response->getHeaderLine('HX-Redirect')); + $this->assertEquals('', $response->getHeaderLine('HX-Location')); + $this->component->clientSideRedirect('/'); + $response = $this->controller->getResponse(); + $this->assertEquals('', $response->getHeaderLine('HX-Redirect')); + $this->assertEquals('/', $response->getHeaderLine('HX-Location')); + } + + /** + * @return void + */ + public function testClientSideRedirectStringFull() + { + $response = $this->controller->getResponse(); + $this->assertEquals('', $response->getHeaderLine('HX-Redirect')); + $this->assertEquals('', $response->getHeaderLine('HX-Location')); + $this->component->clientSideRedirect('/', true); + $response = $this->controller->getResponse(); + $this->assertEquals('/', $response->getHeaderLine('HX-Redirect')); + $this->assertEquals('', $response->getHeaderLine('HX-Location')); + } + + /** + * @return void + */ + public function testClientSideRedirectArray() + { + $response = $this->controller->getResponse(); + $this->assertEquals('', $response->getHeaderLine('HX-Redirect')); + $this->assertEquals('', $response->getHeaderLine('HX-Location')); + $this->component->clientSideRedirect([ + 'controller' => 'Tests', + 'action' => 'index', + ]); + $response = $this->controller->getResponse(); + $this->assertEquals('', $response->getHeaderLine('HX-Redirect')); + $this->assertEquals('/tests', $response->getHeaderLine('HX-Location')); + } + + /** + * @return void + */ + public function testClientSideRedirectArrayFull() + { + $response = $this->controller->getResponse(); + $this->assertEquals('', $response->getHeaderLine('HX-Redirect')); + $this->assertEquals('', $response->getHeaderLine('HX-Location')); + $this->component->clientSideRedirect([ + 'controller' => 'Tests', + 'action' => 'index', + ], true); + $response = $this->controller->getResponse(); + $this->assertEquals('/tests', $response->getHeaderLine('HX-Redirect')); + $this->assertEquals('', $response->getHeaderLine('HX-Location')); + } + + /** + * @return void + */ + public function testClientSideRefresh() + { + $response = $this->controller->getResponse(); + $this->assertEquals('', $response->getHeaderLine('HX-Refresh')); + $this->component->clientSideRefresh(); + $response = $this->controller->getResponse(); + $this->assertEquals('true', $response->getHeaderLine('HX-Refresh')); + } +} diff --git a/tests/test_app/src/Application.php b/tests/test_app/src/Application.php new file mode 100644 index 0000000..2eb4408 --- /dev/null +++ b/tests/test_app/src/Application.php @@ -0,0 +1,34 @@ +scope('/', function(RouteBuilder $routes) { + $routes->fallbacks(DashedRoute::class); + }); + } + + /** + * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to set in your App Class + * @return \Cake\Http\MiddlewareQueue + */ + public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue { + $middlewareQueue->add(new RoutingMiddleware($this)); + + return $middlewareQueue; + } + +}