diff --git a/src/Turbo/src/Helper/TurboStream.php b/src/Turbo/src/Helper/TurboStream.php index daea084e6ab..0c5df0dcaf7 100644 --- a/src/Turbo/src/Helper/TurboStream.php +++ b/src/Turbo/src/Helper/TurboStream.php @@ -86,12 +86,45 @@ public static function refresh(?string $requestId = null): string return \sprintf('', htmlspecialchars($requestId)); } + /** + * Custom action and attributes. + * When passing attributes use null value to for boolean attributes (e.g. disabled). + * + * @param array $attr + */ + public static function custom(string $action, string $target, string $html, array $attr = []): string + { + if (\array_key_exists('action', $attr) || \array_key_exists('targets', $attr)) { + throw new \InvalidArgumentException('The "action" and "targets" attributes are reserved and cannot be used.'); + } + + $attrString = ''; + foreach ($attr as $key => $value) { + $key = htmlspecialchars($key); + if (null === $value) { + $attrString .= \sprintf(' %s', $key); + } elseif (\is_int($value) || \is_float($value)) { + $attrString .= \sprintf(' %s="%s"', $key, $value); + } else { + $attrString .= \sprintf(' %s="%s"', $key, htmlspecialchars($value)); + } + } + + return self::wrap(htmlspecialchars($action), $target, $html, $attrString); + } + private static function wrap(string $action, string $target, string $html, string $attr = ''): string { - return \sprintf(<< - EOHTML, $action, htmlspecialchars($target), $attr, $html); + EOHTML, + $action, + htmlspecialchars($target), + $attr, + $html + ); } } diff --git a/src/Turbo/src/TurboStreamResponse.php b/src/Turbo/src/TurboStreamResponse.php index e597ed30775..4b7beb2f593 100644 --- a/src/Turbo/src/TurboStreamResponse.php +++ b/src/Turbo/src/TurboStreamResponse.php @@ -104,4 +104,16 @@ public function refresh(?string $requestId = null): static return $this; } + + /** + * @param array $attr + * + * @return $this + */ + public function action(string $action, string $target, string $html, array $attr = []): static + { + $this->setContent($this->getContent().TurboStream::custom($action, $target, $html, $attr)); + + return $this; + } } diff --git a/src/Turbo/tests/Helper/TurboStreamTest.php b/src/Turbo/tests/Helper/TurboStreamTest.php index 263e454effd..4fc26dcd6b2 100644 --- a/src/Turbo/tests/Helper/TurboStreamTest.php +++ b/src/Turbo/tests/Helper/TurboStreamTest.php @@ -76,4 +76,32 @@ public function testRefreshWithId(): void TurboStream::refresh('a"b') ); } + + public function testCustom(): void + { + $this->assertSame(<< + + + EOHTML, + TurboStream::custom('customAction', 'some["selector"]', '
content
', ['someAttr' => 'someValue', 'boolAttr' => null, 'intAttr' => 0, 'floatAttr' => 3.14]) + ); + } + + /** + * @dataProvider customThrowsExceptionDataProvider + * + * @param array $attr + */ + public function testCustomThrowsException(string $action, string $target, string $html, array $attr): void + { + $this->expectException(\InvalidArgumentException::class); + TurboStream::custom($action, $target, $html, $attr); + } + + public static function customThrowsExceptionDataProvider(): \Generator + { + yield ['customAction', 'some["selector"]', '
content
', ['action' => 'someAction']]; + yield ['customAction', 'some["selector"]', '
content
', ['targets' => 'someTargets']]; + } }