From e0363b4d9f3a13bc071deb6baed1efa3375b23c2 Mon Sep 17 00:00:00 2001 From: Ben Croker Date: Tue, 20 Aug 2024 20:03:45 +0200 Subject: [PATCH] 3.4.0 --- CHANGELOG.md | 4 +- composer.json | 2 +- src/base/Component.php | 94 ++++++++++++++++++- src/services/RequestsService.php | 69 ++++++++++---- src/variables/SprigVariable.php | 135 +++------------------------- tests/TESTS.md | 2 + tests/pest/Feature/RequestsTest.php | 16 ++++ tests/pest/Feature/VariableTest.php | 3 +- 8 files changed, 178 insertions(+), 147 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ed0365..f3db1fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ # Release Notes for Sprig Core -## 3.3.3 - 2024-08-19 +## 3.4.0 - 2024-08-20 ### Changed +- The `sprig.registerJs(js)` function now executes the registered JavaScript after htmx settles, and is now the recommended way of outputting JavaScript in Sprig components. - Components no longer render markup added via `{% html %}`, `{% css %}` and `{% js %}` tags during Sprig requests. -- The `sprig.registerJs(js)` function now executes the registered JavaScript after htmx settles. ## 3.3.2 - 2024-08-12 diff --git a/composer.json b/composer.json index 0541552..a350d01 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "putyourlightson/craft-sprig-core", "description": "A reactive Twig component framework for Craft.", - "version": "3.3.3", + "version": "3.4.0", "type": "craft-module", "license": "mit", "require": { diff --git a/src/base/Component.php b/src/base/Component.php index 92966e5..fc8b6dd 100644 --- a/src/base/Component.php +++ b/src/base/Component.php @@ -7,8 +7,12 @@ use Craft; use craft\base\Component as BaseComponent; +use craft\helpers\Html; use craft\helpers\Json; use craft\helpers\StringHelper; +use craft\web\View; +use putyourlightson\sprig\services\ComponentsService; +use putyourlightson\sprig\Sprig; abstract class Component extends BaseComponent implements ComponentInterface { @@ -50,7 +54,7 @@ public static function getIsRequest(): bool */ public static function getIsInclude(): bool { - return !self::getIsRequest(); + return !static::getIsRequest(); } /** @@ -186,6 +190,22 @@ public static function refresh(bool $refresh = true): void Craft::$app->getResponse()->getHeaders()->set('HX-Refresh', $refresh ? 'true' : ''); } + /** + * Registers JavaScript code to be output. This method takes care of registering the code in the appropriate way depending on whether it is part of an include or a request. + * + * @since 2.11.0 + */ + public static function registerJs(string $js): void + { + if (static::getIsInclude()) { + Craft::$app->getView()->registerJs($js, View::POS_END); + + return; + } + + Sprig::$core->requests->registerJs($js); + } + /** * Specifies how the response will be swapped. * https://htmx.org/reference#response_headers @@ -204,6 +224,26 @@ public static function retarget(string $target): void Craft::$app->getResponse()->getHeaders()->set('HX-Retarget', $target); } + /** + * Swaps a template out-of-band. Cyclical requests are mitigated by prevented the swapping of unique components multiple times in the current request, including the initiating component. + * https://htmx.org/attributes/hx-swap-oob/ + * + * @since 2.9.0 + */ + public static function swapOob(string $selector, string $template, array $variables = []): void + { + if (static::getIsInclude()) { + return; + } + + $value = Sprig::$core->requests->getOobSwapValue($selector, $template, $variables); + if ($value === null) { + return; + } + + Sprig::$core->requests->registerHtml($value, 'innerHTML:' . $selector); + } + /** * Triggers client-side events. * https://htmx.org/headers/hx-trigger/ @@ -228,4 +268,56 @@ public static function triggerEvents(array|string $events, string $on = 'load'): Craft::$app->getResponse()->getHeaders()->set($header, $events); } } + + /** + * Triggers a refresh event on the provided selector. If variables are provided then they are appended to the component as hidden input fields. Cyclical requests are mitigated by prevented the triggering of unique components multiple times, including the initiating component. + * + * @since 2.9.0 + */ + public static function triggerRefresh(string $selector, array $variables = []): void + { + if (static::getIsInclude()) { + return; + } + + $config = Sprig::$core->requests->getValidatedConfig(); + if (in_array($selector, $config->triggerRefreshSources)) { + return; + } + + $config->triggerRefreshSources[] = '#' . $config->id; + $variables['sprig:triggerRefreshSources'] = Craft::$app->getSecurity()->hashData(Json::encode($config->triggerRefreshSources)); + + foreach ($variables as $name => $value) { + $values[] = Html::hiddenInput($name, $value); + } + + $html = implode('', $values); + Sprig::$core->requests->registerHtml($html, 'beforeend:' . $selector); + Sprig::$core->requests->registerJs('htmx.trigger(\'' . $selector . '\', \'refresh\')'); + } + + /** + * Triggers a refresh event on all components on load. + * https://github.com/putyourlightson/craft-sprig/issues/279 + * + * @since 2.3.0 + */ + public static function triggerRefreshOnLoad(string $selector = ''): void + { + if (static::getIsRequest()) { + return; + } + + $selector = $selector ?: '.' . ComponentsService::SPRIG_CSS_CLASS; + $js = << { + for (const component of htmx.findAll('$selector')) { + htmx.trigger(component, 'refresh'); + } + }); + JS; + + Craft::$app->getView()->registerJs($js); + } } diff --git a/src/services/RequestsService.php b/src/services/RequestsService.php index eff3af9..ddffc3e 100644 --- a/src/services/RequestsService.php +++ b/src/services/RequestsService.php @@ -10,8 +10,6 @@ use craft\helpers\Html; use craft\helpers\Json; use craft\helpers\StringHelper; -use craft\web\View; -use putyourlightson\sprig\base\Component as BaseComponent; use putyourlightson\sprig\models\ConfigModel; use yii\web\BadRequestHttpException; @@ -46,6 +44,13 @@ class RequestsService extends Component */ private array $js = []; + /** + * The components that initiated out-of-band swaps in the current request. + * + * @var string[]|null + */ + private ?array $oobSwapSources = null; + /** * Returns allowed request variables. */ @@ -134,7 +139,7 @@ public function getRegisteredHtml(): string // Execute the JS after htmx settles, at most once. $js = implode(PHP_EOL, $this->js); $content = <<getView()->registerHtml($html); - - return; - } - $this->html[$swapSelector][] = $html; } /** - * Registers JavaScript code to be output. This method takes care of registering the code depending on whether it is part of an include or a request. + * Registers JavaScript code to be output. * * @since 2.11.0 */ public function registerJs(string $js): void { - if (BaseComponent::getIsInclude()) { - Craft::$app->getView()->registerJs($js, View::POS_END); - - return; - } - // Trim any whitespace and ensure it ends with a semicolon. $js = StringHelper::ensureRight(trim($js, " \t\n\r\0\x0B"), ';'); $this->js[] = $js; } + /** + * Returns the value for the out-of-band swap from a rendered template if it exists, otherwise a rendered string. + */ + public function getOobSwapValue(string $selector, string $template, array $variables = []): ?string + { + if (Craft::$app->getView()->resolveTemplate($template) === false) { + return Craft::$app->getView()->renderString($template, $variables); + } + + if (in_array($selector, $this->getOobSwapSources())) { + return null; + } + + $this->oobSwapSources[] = $selector; + + return Craft::$app->getView()->renderTemplate($template, $variables); + } + + /** + * Resets the components that initiated out-of-band swaps in the current request. + */ + public function resetOobSwapSources(): void + { + $this->oobSwapSources = []; + } + /** * Validates if the given data is tampered with and throws an exception if it is. */ @@ -221,4 +240,20 @@ private function getIsVariableAllowed(string $name): bool return true; } + + /** + * Returns the components that initiated out-of-band swaps in the current request, including the original component. + */ + private function getOobSwapSources(): array + { + if ($this->oobSwapSources === null) { + $this->oobSwapSources = []; + $config = $this->getValidatedConfig(); + if ($config->id) { + $this->oobSwapSources[] = '#' . $config->id; + } + } + + return $this->oobSwapSources; + } } diff --git a/src/variables/SprigVariable.php b/src/variables/SprigVariable.php index 2a764b7..f4723eb 100644 --- a/src/variables/SprigVariable.php +++ b/src/variables/SprigVariable.php @@ -5,25 +5,16 @@ namespace putyourlightson\sprig\variables; -use Craft; use craft\db\Paginator; -use craft\helpers\Html; -use craft\helpers\Json; use craft\web\twig\variables\Paginate; use putyourlightson\sprig\base\Component; use putyourlightson\sprig\services\ComponentsService; use putyourlightson\sprig\Sprig; -use Twig\Markup; use yii\db\Query; use yii\web\AssetBundle; class SprigVariable { - /** - * The components that initiated out-of-band swaps in the current request. - */ - private ?array $oobSwapSources = null; - /** * Returns whether this is a Sprig request. */ @@ -198,6 +189,11 @@ public function refresh(bool $refresh = true): void Component::refresh($refresh); } + public function registerJs(string $js): void + { + Component::registerJs($js); + } + public function replaceUrl(string $url): void { Component::replaceUrl($url); @@ -213,132 +209,23 @@ public function retarget(string $target): void Component::retarget($target); } - public function triggerEvents(array|string $events, string $on = 'load'): void + public function swapOob(string $selector, string $template, array $variables = []): void { - Component::triggerEvents($events, $on); + Component::swapOob($selector, $template, $variables); } - /** - * Swaps a template out-of-band. Cyclical requests are mitigated by prevented the swapping of unique components multiple times in the current request, including the initiating component. - * https://htmx.org/attributes/hx-swap-oob/ - * - * @since 2.9.0 - */ - public function swapOob(string $selector, string $template, array $variables = []): void + public function triggerEvents(array|string $events, string $on = 'load'): void { - if (Component::getIsInclude()) { - return; - } - - $value = $this->getOobSwapValue($selector, $template, $variables); - if ($value === null) { - return; - } - - Sprig::$core->requests->registerHtml($value, 'innerHTML:' . $selector); + Component::triggerEvents($events, $on); } - /** - * Triggers a refresh event on the provided selector. If variables are provided then they are appended to the component as hidden input fields. Cyclical requests are mitigated by prevented the triggering of unique components multiple times, including the initiating component. - * - * @since 2.9.0 - */ public function triggerRefresh(string $selector, array $variables = []): void { - if (Component::getIsInclude()) { - return; - } - - $config = Sprig::$core->requests->getValidatedConfig(); - if (in_array($selector, $config->triggerRefreshSources)) { - return; - } - - $config->triggerRefreshSources[] = '#' . $config->id; - $variables['sprig:triggerRefreshSources'] = Craft::$app->getSecurity()->hashData(Json::encode($config->triggerRefreshSources)); - - foreach ($variables as $name => $value) { - $values[] = Html::hiddenInput($name, $value); - } - - $html = implode('', $values); - Sprig::$core->requests->registerHtml($html, 'beforeend:' . $selector); - Sprig::$core->requests->registerJs('htmx.trigger(\'' . $selector . '\', \'refresh\')'); + Component::triggerRefresh($selector, $variables); } - /** - * Triggers a refresh event on all components on load. - * https://github.com/putyourlightson/craft-sprig/issues/279 - * - * @since 2.3.0 - */ public function triggerRefreshOnLoad(string $selector = ''): void { - if (Component::getIsRequest()) { - return; - } - - $selector = $selector ?: '.' . ComponentsService::SPRIG_CSS_CLASS; - $js = << { - for (const component of htmx.findAll('$selector')) { - htmx.trigger(component, 'refresh'); - } - }); - JS; - - Sprig::$core->requests->registerJs($js); - } - - /** - * Registers JavaScript code to be executed. This method takes care of registering the code in the appropriate way depending on whether it is part of an include or a request. - * - * @since 2.10.0 - */ - public function registerJs(string $js): void - { - Sprig::$core->requests->registerJs($js); - } - - /** - * Returns a new component. - */ - public function getComponent(string $value, array $variables = [], array $attributes = []): Markup - { - return Sprig::$core->components->create($value, $variables, $attributes); - } - - /** - * Returns the value for the out-of-band swap from a rendered template if it exists, otherwise a rendered string. - */ - private function getOobSwapValue(string $selector, string $template, array $variables = []): ?string - { - if (Craft::$app->getView()->resolveTemplate($template) === false) { - return Craft::$app->getView()->renderString($template, $variables); - } - - if (in_array($selector, $this->getOobSwapSources())) { - return null; - } - - $this->oobSwapSources[] = $selector; - - return Craft::$app->getView()->renderTemplate($template, $variables); - } - - /** - * Returns the components that initiated out-of-band swaps in the current request, including the original component. - */ - private function getOobSwapSources(): array - { - if ($this->oobSwapSources === null) { - $this->oobSwapSources = []; - $config = Sprig::$core->requests->getValidatedConfig(); - if ($config->id) { - $this->oobSwapSources[] = '#' . $config->id; - } - } - - return $this->oobSwapSources; + Component::triggerRefreshOnLoad($selector); } } diff --git a/tests/TESTS.md b/tests/TESTS.md index 1765ae9..5a0da0b 100644 --- a/tests/TESTS.md +++ b/tests/TESTS.md @@ -69,6 +69,8 @@ _Tests the handling of component requests._ ![Pass](https://raw.githubusercontent.com/putyourlightson/craft-generate-test-spec/main/icons/pass.svg) Get cache duration provided a decimal. ![Pass](https://raw.githubusercontent.com/putyourlightson/craft-generate-test-spec/main/icons/pass.svg) Get cache duration when false. ![Pass](https://raw.githubusercontent.com/putyourlightson/craft-generate-test-spec/main/icons/pass.svg) Get cache duration provided a negative value. +![Pass](https://raw.githubusercontent.com/putyourlightson/craft-generate-test-spec/main/icons/pass.svg) Get registered HTML. +![Pass](https://raw.githubusercontent.com/putyourlightson/craft-generate-test-spec/main/icons/pass.svg) Get registered HTML includes registered JS. ![Pass](https://raw.githubusercontent.com/putyourlightson/craft-generate-test-spec/main/icons/pass.svg) Validate data. ### [Script](pest/Feature/ScriptTest.php) diff --git a/tests/pest/Feature/RequestsTest.php b/tests/pest/Feature/RequestsTest.php index ed68cf0..fb615d2 100644 --- a/tests/pest/Feature/RequestsTest.php +++ b/tests/pest/Feature/RequestsTest.php @@ -87,6 +87,22 @@ ->toBe(0); }); +test('Get registered HTML', function() { + Sprig::$core->requests->registerHtml('xyz', 'swap:selector'); + + expect(Sprig::$core->requests->getRegisteredHtml()) + ->toContain('swap-oob="swap:selector"') + ->toContain('xyz'); +}); + +test('Get registered HTML includes registered JS', function() { + Sprig::$core->requests->registerJs('xyz'); + + expect(Sprig::$core->requests->getRegisteredHtml()) + ->toContain('swap-oob="beforeend:body"') + ->toContain('xyz'); +}); + test('Validate data', function() { $data = Craft::$app->getSecurity()->hashData('xyz') . 'abc'; diff --git a/tests/pest/Feature/VariableTest.php b/tests/pest/Feature/VariableTest.php index 6b9e6a9..8b596f3 100644 --- a/tests/pest/Feature/VariableTest.php +++ b/tests/pest/Feature/VariableTest.php @@ -12,7 +12,6 @@ beforeEach(function() { Sprig::bootstrap(); Craft::$app->getResponse()->getHeaders()->removeAll(); - Craft::$app->getView()->clear(); Craft::$app->getView()->setTemplatesPath(Craft::getAlias('@putyourlightson/sprig/test/templates')); }); @@ -50,7 +49,7 @@ 'sprig:config' => Craft::$app->getSecurity()->hashData('{"template":"test"}'), ]); Craft::$app->getRequest()->getHeaders()->set('HX-Request', true); - Craft::$app->getView()->clear(); + Sprig::$core->requests->resetOobSwapSources(); }); test('Swap OOB', function() {