From 5afb4335798cdc5e876d7a6c0dd2c38e7cec3caa Mon Sep 17 00:00:00 2001 From: Juhani Aronen Date: Sat, 10 Aug 2024 12:36:55 +0300 Subject: [PATCH 1/2] Update HtmxComponent.php --- src/Controller/Component/HtmxComponent.php | 101 ++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/src/Controller/Component/HtmxComponent.php b/src/Controller/Component/HtmxComponent.php index f783374..d1ab026 100644 --- a/src/Controller/Component/HtmxComponent.php +++ b/src/Controller/Component/HtmxComponent.php @@ -18,6 +18,13 @@ class HtmxComponent extends Component */ protected $_defaultConfig = []; + /** + * List of blocks that will be rendered. + * + * @var array + */ + protected array $blocks = []; + /** * List of triggers to use on request * @@ -39,6 +46,19 @@ class HtmxComponent extends Component */ private array $triggersAfterSwap = []; + /** + * Get the callbacks this class is interested in. + * + * @return array + */ + public function implementedEvents(): array + { + return [ + 'Controller.beforeRender' => 'beforeRender', + 'View.afterRender' => 'afterRender', + ]; + } + /** * Initialize properties. * @@ -54,13 +74,45 @@ public function initialize(array $config): void * * @return void */ - public function beforeRender(): void + public function beforeRender($event): void { if ($this->getController()->getRequest()->is('htmx')) { $this->prepare(); } } + /** + * afterRender callback. + * + * If setBlock is used this will render the set block if it exists + * + * @return void + */ + public function afterRender($event) + { + if (!empty($this->blocks)) { + // empty the content and replace with the ones we want + $event->getSubject()->assign('content', ''); + + $first = true; + foreach ($this->blocks as $key => $block) { + if ($event->getSubject()->exists($block)) { + $fetchBlock = $event->getSubject()->fetch($block); + + if (!$first) { + $fetchBlock = preg_replace('/(<[^\/][^>]*)(>)/', '$1 hx-swap-oob="innerHTML"$2', $fetchBlock, 1); + } + + $event->getSubject()->append('content', $fetchBlock); + + if ($first) { + $first = false; + } + } + } + } + } + /** * The current URL of the browser when the htmx request was made. * @@ -322,4 +374,51 @@ private function encodeTriggers(array $triggers): string return implode(',', array_keys($triggers)); } + + /** + * Set a specific block to render + * Removes other blocks that might be rendered + * + * @param string|null $block Name of the block + */ + public function setBlock(?string $block): static + { + $this->blocks = [$block]; + + return $this; + } + + /** + * Add a specific block to render + * + * @param string $block Name of the block + */ + public function addBlock(string $block): static + { + $this->blocks[] = $block; + + return $this; + } + + /** + * Add blocks to render + * + * @param array $block List of block names to render + */ + public function addBlocks(array $block): static + { + $this->blocks[] = $block; + + return $this; + } + + /** + * Get the block that will be rendered + * + * @return string|null + */ + public function getBlocks(): ?string + { + return $this->blocks; + } } From feba0f5ac9d0f0e3778533ae5571837367b9a21d Mon Sep 17 00:00:00 2001 From: Juhani Aronen Date: Sat, 10 Aug 2024 12:40:34 +0300 Subject: [PATCH 2/2] Add tutorial to README --- README.md | 281 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) diff --git a/README.md b/README.md index d43929e..2442b0e 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,287 @@ document.body.addEventListener('htmx:configRequest', (event) => { event.detail.headers['X-CSRF-Token'] = "getRequest()->getAttribute('csrfToken') ?>"; }) ``` +## Rendering blocks and OOB Swap +The `setBlock()` function allows you to render a specific block while removing other blocks that might be rendered. This is particularly useful when you need to update only a portion of your view. + +```php +$this->Htmx->setBlock('userTable'); +``` +The `addBlock()` function allows you to add a specific block to the list of blocks that should be rendered. + +```php +$this->Htmx->addBlock('userTable'); +``` +The `addBlocks()` function allows you to add multiple blocks to the list of blocks that should be rendered +```php +$this->Htmx->addBlocks(['userTable', 'pagination']); +``` + +### OOB Swap +Htmx supports updating multiple targets by returning multiple partial responses with [`hx-swap-oop`](https://htmx.org/docs/#oob_swaps). +See the example `Users index search functionality with pagination update` +Note if you are working with tables like in the example. You might need to add +```javascript + +``` +In your template or layout. + +## Examples + +### Users index search functionality + +In this example, we will implement a search functionality for the users' index using Htmx to filter results dynamically. We will wrap our table body inside a [viewBlock](https://book.cakephp.org/5/en/views.html#using-view-blocks) called `usersTable`. When the page loads, we will render the `usersTable` [viewBlock](https://book.cakephp.org/5/en/views.html#using-view-blocks). + +```php +// Template/Users/index.php + +Form->control('search', [ + 'label' => false, + 'placeholder' => __('Search'), + 'type' => 'text', + 'required' => false, + 'class' => 'form-control input-text search', + 'value' => !empty($search) ? $search : '', + 'hx-get' => $this->Url->build(['controller' => 'Users', 'action' => 'index']), + 'hx-trigger' => "keyup changed delay:200ms", + 'hx-target' => "#search-results", + 'templates' => [ + 'inputContainer' => '
{{content}}
' + ] +]); ?> + + + + + + + + + + + + + + + start('usersTable'); ?> + + + + + + + + + + + end(); ?> + + fetch('usersTable'); ?> + +
id ?>name) ?>email) ?>modified ?>created ?> + Html->link('Edit', + [ + 'action' => 'edit', + $user->id + ], + [ + 'escape' => false + ] + ); ?> + Form->postLink('Delete', + [ + 'action' => 'delete', + $user->id + ], + [ + 'confirm' => __('Are you sure you want to delete user {0}?', $user->email), + 'escape' => false + ] + ); ?> +
+``` +In out controller we will check if the request is Htmx and if so then we will only render the `usersTable` [viewBlock](https://book.cakephp.org/5/en/views.html#using-view-blocks). + +```php +// src/Controller/UsersController.php + +public function index() +{ + $search = null; + $query = $this->Users->find('all'); + + if ($this->request->is('get')) { + if(!empty($this->request->getQueryParams())) { + $data = $this->request->getQueryParams(); + + if(isset($data['search'])) { + $data = $data['search']; + $conditions = [ + 'OR' => [ + 'Users.id' => (int)$data, + 'Users.name LIKE' => '%' . $data . '%', + 'Users.email LIKE' => '%' . $data . '%', + ], + ]; + $query = $query->where([$conditions]); + $search = $data; + } + } + } + + $this->paginate['limit'] = 200; + $users = $this->paginate($query); + $this->set(compact('users', 'search')); + + if($this->getRequest()->is('htmx')) { + $this->viewBuilder()->disableAutoLayout(); + + // we will only render the usersTable viewblock + $this->Htmx->setBlock('usersTable'); + } +} +``` + +### Users index search functionality with pagination update +In this example, we will implement a dynamic search functionality for the users' index using Htmx. This will allow us to filter results in real-time and update pagination accordingly. We will wrap our table body inside a [viewBlock](https://book.cakephp.org/5/en/views.html#using-view-blocks) called `usersTable` and our pagination to `pagination` block. When the page loads, we will render both the `usersTable` and `pagination` [viewBlock](https://book.cakephp.org/5/en/views.html#using-view-blocks). + +```php +// Template/Users/index.php + +Form->control('search', [ + 'label' => false, + 'placeholder' => __('Search'), + 'type' => 'text', + 'required' => false, + 'class' => 'form-control input-text search', + 'value' => !empty($search) ? $search : '', + 'hx-get' => $this->Url->build(['controller' => 'Users', 'action' => 'index']), + 'hx-trigger' => 'keyup changed delay:200ms', + 'hx-target' => '#search-results', + 'hx-push-url' => 'true', + 'templates' => [ + 'inputContainer' => '
{{content}}
' + ] +]); ?> + + + + + + + + + + + + + + + start('usersTable'); ?> + + + + + + + + + + + end(); ?> + + fetch('usersTable'); ?> + +
id ?>name) ?>email) ?>modified ?>created ?> + Html->link('Edit', + [ + 'action' => 'edit', + $user->id + ], + [ + 'escape' => false + ] + ); ?> + Form->postLink('Delete', + [ + 'action' => 'delete', + $user->id + ], + [ + 'confirm' => __('Are you sure you want to delete user {0}?', $user->email), + 'escape' => false + ] + ); ?> +
+ +// pagination +start('pagination'); ?> + +end(); ?> + +fetch('pagination'); ?> +``` +In out controller we will check if the request is Htmx and if so then we will only render the `usersTable` [viewBlock](https://book.cakephp.org/5/en/views.html#using-view-blocks). + +```php +// src/Controller/UsersController.php + +public function index() +{ + $search = null; + $query = $this->Users->find('all'); + + if ($this->request->is('get')) { + if(!empty($this->request->getQueryParams())) { + $data = $this->request->getQueryParams(); + + if(isset($data['search'])) { + $data = $data['search']; + $conditions = [ + 'OR' => [ + 'Users.id' => (int)$data, + 'Users.name LIKE' => '%' . $data . '%', + 'Users.email LIKE' => '%' . $data . '%', + ], + ]; + $query = $query->where([$conditions]); + $search = $data; + } + } + } + + $this->paginate['limit'] = 200; + $users = $this->paginate($query); + $this->set(compact('users', 'search')); + + if($this->getRequest()->is('htmx')) { + $this->viewBuilder()->disableAutoLayout(); + + // render users table and pagination blocks + $this->Htmx->addBlock('usersTable')->addBlock('pagination'); + } +} +``` ## License