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(<<
%s
- 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(<<
+ content
+
+ 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']];
+ }
}