diff --git a/CHANGELOG.md b/CHANGELOG.md index b6fc3ed..502465e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # Changelog -## [Unreleased](https://github.com/clickfwd/yoyo/compare/0.9.0...develop) +## [Unreleased](https://github.com/clickfwd/yoyo/compare/0.9.1...develop) + +## [0.9.1 (2024-04-16)](https://github.com/clickfwd/yoyo/compare/0.9.0...0.9.1) + +- Fix Safari/iOS errors due to evt.target and evt.srcElement now being null. +- Add support for port in UrlStateManagerService.php +- PHP 8.2/8.3 compat +- Fix ResponseHeaders::refresh error due to missing parameter. +- Fix headers already sent error when setting status code in response +- Ensure components are compiled only once. +- Bump htmx to v1.9.4 and include new config options. +- New Request::set, Request::triggerName and Request::header methods. +- New Response::reselect method for the HX-Reselect header. +- New New Yoyo::actionArgs method. ## [0.9.0 (2023-04-02)](https://github.com/clickfwd/yoyo/compare/0.8.1...0.9.0) diff --git a/composer.json b/composer.json index 33b8480..73df484 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "framework", "yoyo" ], - "version": "0.9.0", + "version": "0.9.1", "license": "MIT", "homepage": "https://github.com/Clickfwd/yoyo", "support": { @@ -20,8 +20,8 @@ "minimum-stability": "dev", "prefer-stable": true, "require": { - "php": "^7.3|^8.0|^8.1", - "illuminate/container": "^8.0||^9.0" + "php": "^7.3|^8.0|^8.1|^8.2|^8.3", + "illuminate/container": "^8.0||^9.0||^10.0" }, "require-dev": { "phpunit/phpunit": "^9.3", diff --git a/src/assets/js/yoyo.js b/src/assets/js/yoyo.js index 6dd933f..695da94 100644 --- a/src/assets/js/yoyo.js +++ b/src/assets/js/yoyo.js @@ -43,23 +43,23 @@ }, afterProcessNode(evt) { // Create non-existent target - if (evt.srcElement) { + if (evt.detail.elt) { this.createNonExistentIdTarget( - evt.srcElement.getAttribute('hx-target') + evt.detail.elt.getAttribute('hx-target') ) } // Initialize spinners let component - if (!evt.srcElement || !isComponent(evt.srcElement)) { + if (!evt.detail.elt || !isComponent(evt.detail.elt)) { // For innerHTML swap find the component root node component = YoyoEngine.closest( evt.detail.elt, '[hx-swap~=innerHTML]' ) } else { - component = getComponent(evt.srcElement) + component = getComponent(evt.detail.elt) } if (!component) { @@ -69,7 +69,7 @@ initializeComponentSpinners(component) }, bootstrapRequest(evt) { - const elt = evt.target + const elt = evt.detail.elt let component = getComponent(elt) const componentName = getComponentName(component) @@ -133,13 +133,13 @@ let component = getComponentById(evt.detail.target.id) if (!component) { - if (!evt.target) { + if (!evt.detail.elt) { return } // Needed when using yoyo:select to replace a specific part of the response // so stop spinning callbacks are run to remove animations in the parts of the component // that were not replaced - component = getComponent(evt.target) + component = getComponent(evt.detail.elt) if (component) { spinningStop(component) } @@ -713,7 +713,7 @@ YoyoEngine.defineExtension('yoyo', { } if (name === 'htmx:configRequest') { - if (!evt.target) return + if (!evt.detail.elt) return Yoyo.bootstrapRequest(evt) } @@ -761,7 +761,7 @@ YoyoEngine.defineExtension('yoyo', { } if (name === 'htmx:beforeSwap') { - if (!evt.target) return + if (!evt.detail.elt) return // Add triggering element info to event detail so it can be read in after swap events // For example to push the href url to browser history using the href from the element that's no longer present on the page @@ -794,7 +794,7 @@ YoyoEngine.defineExtension('yoyo', { // Push component response to history cache // Make sure we trigger once for the new element - this was failing in Safari mobile // Causing a duplicate snapshot - if (!evt.target || !evt.target.isConnected) return + if (!evt.detail.elt || !evt.detail.elt.isConnected) return Yoyo.afterSettleActions(evt) } diff --git a/src/yoyo/Concerns/ResponseHeaders.php b/src/yoyo/Concerns/ResponseHeaders.php index 47c3628..3a4a722 100644 --- a/src/yoyo/Concerns/ResponseHeaders.php +++ b/src/yoyo/Concerns/ResponseHeaders.php @@ -27,12 +27,12 @@ public function redirect($url) public function refresh() { - $this->header('HX-Refresh'); + $this->header('HX-Refresh', 'true'); return $this; } - public function replace($url) + public function replaceUrl($url) { $this->header('HX-Replace-Url', $url); @@ -46,6 +46,13 @@ public function reswap($swap) return $this; } + public function reselect($selector) + { + $this->header('HX-Reselect', $selector); + + return $this; + } + public function retarget($selector) { $this->header('HX-Retarget', $selector); diff --git a/src/yoyo/QueryString.php b/src/yoyo/QueryString.php index 2b23ccf..b58bf46 100644 --- a/src/yoyo/QueryString.php +++ b/src/yoyo/QueryString.php @@ -63,6 +63,10 @@ public function getPageQueryParams() // If a query string value matches the default value, remove it from the URL foreach ($queryParams as $key => $val) { + if (is_object($val) && method_exists($val, 'toArray')) { + $queryParams[$key] = $val->toArray(); + } + if (isset($this->defaults[$key]) && $val === $this->defaults[$key] || $val === '') { unset($queryParams[$key]); } diff --git a/src/yoyo/Request.php b/src/yoyo/Request.php index cf83b2c..ea43546 100644 --- a/src/yoyo/Request.php +++ b/src/yoyo/Request.php @@ -98,6 +98,20 @@ public function startsWith($prefix) return $vars; } + public function set($key, $value) + { + $this->request[$key] = $value; + + return $this; + } + + public function merge($data) + { + $this->request = array_merge($this->request, $data); + + return $this; + } + public function drop($key) { $this->dropped[] = $key; @@ -145,4 +159,14 @@ public function triggerId() { return $this->server['HTTP_HX_TRIGGER']; } + + public function triggerName() + { + return $this->server['HTTP_HX_TRIGGER_NAME'] ?? null; + } + + public function header($name) + { + return $this->server['HTTP_'.strtoupper($name)] ?? null; + } } diff --git a/src/yoyo/Services/Configuration.php b/src/yoyo/Services/Configuration.php index 2c281fd..594fccf 100644 --- a/src/yoyo/Services/Configuration.php +++ b/src/yoyo/Services/Configuration.php @@ -10,31 +10,35 @@ class Configuration private static $options; - public static $htmx = '1.8.4'; + public static $htmx = '1.9.4'; protected static $allowedConfigOptions = [ - 'addedClass', - 'allowEval', - 'attributesToSettle', - 'defaultFocusScroll', - 'defaultSettleDelay', - 'defaultSwapDelay', - 'defaultSwapStyle', - 'disableSelector', - 'historyCacheSize', 'historyEnabled', + 'historyCacheSize', + 'refreshOnHistoryMiss', + 'defaultSwapStyle', + 'defaultSwapDelay', + 'defaultSettleDelay', 'includeIndicatorStyles', 'indicatorClass', - 'inlineScriptNonce', - 'refreshOnHistoryMiss', 'requestClass', - 'scrollBehavior', + 'addedClass', 'settlingClass', 'swappingClass', - 'timeout', - 'useTemplateFragments', + 'allowEval', + 'inlineScriptNonce', + 'attributesToSettle', 'withCredentials', + 'timeout', 'wsReconnectDelay', + 'wsBinaryType', + 'disableSelector', + 'useTemplateFragments', + 'scrollBehavior', + 'defaultFocusScroll', + 'getCacheBusterParam', + 'globalViewTransitions', + 'methodsThatUseUrlParams', ]; public function __construct($options) diff --git a/src/yoyo/Services/Response.php b/src/yoyo/Services/Response.php index 8ff75d1..70c3f46 100644 --- a/src/yoyo/Services/Response.php +++ b/src/yoyo/Services/Response.php @@ -41,12 +41,11 @@ public function send(string $content = ''): string header("$key: $value"); } - if ($this->statusCode == 204) { - http_response_code(204); + // Prevent headers already sent error + if (! headers_sent()) { + http_response_code($this->statusCode ?? 200); } - http_response_code($this->statusCode ?? 200); - return $content ?: ''; } diff --git a/src/yoyo/Services/UrlStateManagerService.php b/src/yoyo/Services/UrlStateManagerService.php index b06494e..d39c865 100644 --- a/src/yoyo/Services/UrlStateManagerService.php +++ b/src/yoyo/Services/UrlStateManagerService.php @@ -27,7 +27,8 @@ public function pushState($queryParams) $parsedUrl = parse_url($this->currentUrl); - $url = $parsedUrl['scheme'].'://'.$parsedUrl['host'].$parsedUrl['path'].($queryParams ? '?'.http_build_query($queryParams) : ''); + $port = isset($parsedUrl['port']) ? (':'.$parsedUrl['port']) : ''; + $url = $parsedUrl['scheme'].'://'.$parsedUrl['host'].$port.$parsedUrl['path'].($queryParams ? '?'.http_build_query($queryParams) : ''); if ($url !== $this->currentUrl) { $response->header('Yoyo-Push', $url); diff --git a/src/yoyo/Yoyo.php b/src/yoyo/Yoyo.php index 6226317..dd59dd1 100644 --- a/src/yoyo/Yoyo.php +++ b/src/yoyo/Yoyo.php @@ -204,6 +204,13 @@ public function action($action): self return $this; } + public function actionArgs(...$args) + { + $this->request()->merge(['actionArgs' => $args]); + + return $this; + } + /** * Renders the component on initial page load. */ diff --git a/src/yoyo/YoyoCompiler.php b/src/yoyo/YoyoCompiler.php index 409fed6..1dc1ee0 100644 --- a/src/yoyo/YoyoCompiler.php +++ b/src/yoyo/YoyoCompiler.php @@ -131,11 +131,11 @@ public function compile($html): string $prefix_finder = self::YOYO_PREFIX_FINDER; // U modifier needed to match children tags when there are no line breaks in the HTML code - $html = preg_replace('/ '.$prefix.':(.*)="(.*)"/U', " $prefix_finder $prefix:\$1=\"\$2\"", $html); $html = preg_replace('/ ' . $prefix . ':(.*)=\'(.*)\'/U', " {$prefix_finder} {$prefix}:\$1='\$2'", $html); - $html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'); + // Converts non-ascii characters to numeric html entities + $html = mb_encode_numericentity($html, [0x80, 0x10FFFF, 0, ~0], 'UTF-8'); $dom = new DOMDocument(); @@ -193,6 +193,11 @@ protected function addComponentRootAttributes($element) return; } + // Skip when component already compiled + if ($element->hasAttribute(self::yoprefix('name')) && $element->hasAttribute(self::hxprefix('vals'))) { + return; + } + $element->setAttribute(self::YOYO_PREFIX, ''); $element->setAttribute(self::YOYO_PREFIX_FINDER, ''); diff --git a/tests/Unit/YoyoCompileTest.php b/tests/Unit/YoyoCompileTest.php index 03fe79b..6e4874c 100644 --- a/tests/Unit/YoyoCompileTest.php +++ b/tests/Unit/YoyoCompileTest.php @@ -126,3 +126,13 @@ expect(compile_html_with_vars('foo', '
', ['foo' => 'bar'])) ->toContain(hxattr('vals', encode_vals([yoprefix_value('id') => 'foo', 'foo' => 'bar']))); }); + +it('correctly compiles component with non-ascii characters', function () { + expect(compile_html('foo', '

áéíóü

')) + ->toContain('áéíóü'); +}); + +it('correctly compiles component with Chinese characters', function () { + expect(compile_html('foo', '

极简、极速、极致、 海豚PHP、PHP开发框架、后台框架

')) + ->toContain('极简、极速、极致、 海豚PHP、PHP开发框架、后台框架'); +}); diff --git a/tests/app/Yoyo/ActionArguments.php b/tests/app/Yoyo/ActionArguments.php index 90e5558..f8ba8cf 100644 --- a/tests/app/Yoyo/ActionArguments.php +++ b/tests/app/Yoyo/ActionArguments.php @@ -6,6 +6,10 @@ class ActionArguments extends Component { + protected $a; + + protected $b; + public function someAction($a, $b) { $this->a = $a; diff --git a/tests/app/Yoyo/CounterDynamicProperties.php b/tests/app/Yoyo/CounterDynamicProperties.php index 8cea367..a33875d 100644 --- a/tests/app/Yoyo/CounterDynamicProperties.php +++ b/tests/app/Yoyo/CounterDynamicProperties.php @@ -4,6 +4,7 @@ use Clickfwd\Yoyo\Component; +#[\AllowDynamicProperties] class CounterDynamicProperties extends Component { public function getQueryString() @@ -11,6 +12,11 @@ public function getQueryString() return $this->getDynamicProperties(); } + /** + * The 'count' property value is not known ahead of time and can be set programatically; + * + * @return void + */ public function getDynamicProperties() { return ['count']; diff --git a/tests/app/Yoyo/DependencyInjectionClassWithNamedArgumentMapping.php b/tests/app/Yoyo/DependencyInjectionClassWithNamedArgumentMapping.php index f668f2a..eda1b93 100644 --- a/tests/app/Yoyo/DependencyInjectionClassWithNamedArgumentMapping.php +++ b/tests/app/Yoyo/DependencyInjectionClassWithNamedArgumentMapping.php @@ -8,6 +8,8 @@ class DependencyInjectionClassWithNamedArgumentMapping extends Component { + protected $id; + protected $post; // $foo variable passed to component is automaticaly injected in Post::__constructor