Skip to content

Commit 9c9f58c

Browse files
committed
feature #2298 [Turbo] Support custom TurboStreamResponse actions (DRaichev)
This PR was merged into the 2.x branch. Discussion ---------- [Turbo] Support custom TurboStreamResponse actions | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Issues | - | License | MIT The current implementation of TurboStream & TurboStreamResponse has no generic action (only the predefined turbo actions) and what's more the class is marked as final, which does not allow adding support for more actions. I think the wrap function should be public or have a public function "custom" that exposes the functionality (I've opted for the latter) I really like this library: https://github.com/marcoroth/turbo_power It adds additional simple actions. For example I use set/delete attributes to enable/disable UI elements This change will allow people to use this or similar libraries, or even build their own custom actions if they want to. There is no impact to the rest of the functionality or DX There is a minor change to the wrap function, it now accepts an array of attributes instead of a string. It builds the string from the array of attributes, and adds a leading space. This aims to prevent errors, as the previous implementation needs the $attr argument to start with a blank space otherwise it would produce invalid html Commits ------- d29e4d9 Add support for custom actions in `TurboStream` and `TurboStreamResponse`
2 parents eb8dd83 + d29e4d9 commit 9c9f58c

File tree

4 files changed

+73
-0
lines changed

4 files changed

+73
-0
lines changed

src/Turbo/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Add `<twig:Turbo:Stream>` component
66
- Add `<twig:Turbo:Frame>` component
7+
- Add support for custom actions in `TurboStream` and `TurboStreamResponse`
78

89
## 2.21.0
910

src/Turbo/src/Helper/TurboStream.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,34 @@ public static function refresh(?string $requestId = null): string
8686
return \sprintf('<turbo-stream action="refresh" request-id="%s"></turbo-stream>', htmlspecialchars($requestId));
8787
}
8888

89+
/**
90+
* Custom action and attributes.
91+
*
92+
* Set boolean attributes (e.g., `disabled`) by providing the attribute name as key with `null` as value.
93+
*
94+
* @param array<string, string|int|float|null> $attr
95+
*/
96+
public static function action(string $action, string $target, string $html, array $attr = []): string
97+
{
98+
if (\array_key_exists('action', $attr) || \array_key_exists('targets', $attr)) {
99+
throw new \InvalidArgumentException('The "action" and "targets" attributes are reserved and cannot be used.');
100+
}
101+
102+
$attrString = '';
103+
foreach ($attr as $key => $value) {
104+
$key = htmlspecialchars($key);
105+
if (null === $value) {
106+
$attrString .= \sprintf(' %s', $key);
107+
} elseif (\is_int($value) || \is_float($value)) {
108+
$attrString .= \sprintf(' %s="%s"', $key, $value);
109+
} else {
110+
$attrString .= \sprintf(' %s="%s"', $key, htmlspecialchars($value));
111+
}
112+
}
113+
114+
return self::wrap(htmlspecialchars($action), $target, $html, $attrString);
115+
}
116+
89117
private static function wrap(string $action, string $target, string $html, string $attr = ''): string
90118
{
91119
return \sprintf(<<<EOHTML

src/Turbo/src/TurboStreamResponse.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,20 @@ public function refresh(?string $requestId = null): static
104104

105105
return $this;
106106
}
107+
108+
/**
109+
* Custom action and attributes.
110+
*
111+
* Set boolean attributes (e.g., `disabled`) by providing the attribute name as key with `null` as value.
112+
*
113+
* @param array<string, string|int|float|null> $attr
114+
*
115+
* @return $this
116+
*/
117+
public function action(string $action, string $target, string $html, array $attr = []): static
118+
{
119+
$this->setContent($this->getContent().TurboStream::action($action, $target, $html, $attr));
120+
121+
return $this;
122+
}
107123
}

src/Turbo/tests/Helper/TurboStreamTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,32 @@ public function testRefreshWithId(): void
7676
TurboStream::refresh('a"b')
7777
);
7878
}
79+
80+
public function testCustom(): void
81+
{
82+
$this->assertSame(<<<EOHTML
83+
<turbo-stream action="customAction" targets="some[&quot;selector&quot;]" someAttr="someValue" boolAttr intAttr="0" floatAttr="3.14">
84+
<template><div>content</div></template>
85+
</turbo-stream>
86+
EOHTML,
87+
TurboStream::action('customAction', 'some["selector"]', '<div>content</div>', ['someAttr' => 'someValue', 'boolAttr' => null, 'intAttr' => 0, 'floatAttr' => 3.14])
88+
);
89+
}
90+
91+
/**
92+
* @dataProvider customThrowsExceptionDataProvider
93+
*
94+
* @param array<string, string|int|float|null> $attr
95+
*/
96+
public function testCustomThrowsException(string $action, string $target, string $html, array $attr): void
97+
{
98+
$this->expectException(\InvalidArgumentException::class);
99+
TurboStream::action($action, $target, $html, $attr);
100+
}
101+
102+
public static function customThrowsExceptionDataProvider(): \Generator
103+
{
104+
yield ['customAction', 'some["selector"]', '<div>content</div>', ['action' => 'someAction']];
105+
yield ['customAction', 'some["selector"]', '<div>content</div>', ['targets' => 'someTargets']];
106+
}
79107
}

0 commit comments

Comments
 (0)