diff --git a/src/Turbo/CHANGELOG.md b/src/Turbo/CHANGELOG.md index b5ad566cf65..d3e4d1c9eeb 100644 --- a/src/Turbo/CHANGELOG.md +++ b/src/Turbo/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## 2.21.0 + +- Add `Helper/TurboStream::append()` et al. methods +- Add `TurboStreamResponse` + ## 2.19.0 - Fix Doctrine proxies are not Broadcasted #3139 diff --git a/src/Turbo/doc/index.rst b/src/Turbo/doc/index.rst index d20f6c73ed0..336145ca68c 100644 --- a/src/Turbo/doc/index.rst +++ b/src/Turbo/doc/index.rst @@ -374,7 +374,7 @@ Let's discover how to use Turbo Streams to enhance your `Symfony forms`_:: {% endblock %} Supported actions are ``append``, ``prepend``, ``replace``, ``update``, -``remove``, ``before`` and ``after``. +``remove``, ``before``, ``after`` and ``refresh``. `Read the Turbo Streams documentation for more details`_. Resetting the Form diff --git a/src/Turbo/phpstan.neon.dist b/src/Turbo/phpstan.neon.dist index 62d57c07f1f..74ef7289547 100644 --- a/src/Turbo/phpstan.neon.dist +++ b/src/Turbo/phpstan.neon.dist @@ -45,3 +45,8 @@ parameters: message: "#^Call to an undefined method Doctrine\\\\ORM\\\\Event\\\\PostFlushEventArgs\\:\\:getEntityManager\\(\\)\\.$#" count: 1 path: src/Doctrine/BroadcastListener.php + + - + message: "#^Method Symfony\\\\UX\\\\Turbo\\\\TurboStreamResponse::__construct\\(\\) has parameter \\$headers with no value type specified in iterable type array\\.$#" + count: 1 + path: src/TurboStreamResponse.php diff --git a/src/Turbo/src/Helper/TurboStream.php b/src/Turbo/src/Helper/TurboStream.php new file mode 100644 index 00000000000..daea084e6ab --- /dev/null +++ b/src/Turbo/src/Helper/TurboStream.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Turbo\Helper; + +/** + * @see https://turbo.hotwired.dev/reference/streams + */ +final class TurboStream +{ + /** + * Appends to the element(s) designated by the target CSS selector. + */ + public static function append(string $target, string $html): string + { + return self::wrap('append', $target, $html); + } + + /** + * Prepends to the element(s) designated by the target CSS selector. + */ + public static function prepend(string $target, string $html): string + { + return self::wrap('prepend', $target, $html); + } + + /** + * Replaces the element(s) designated by the target CSS selector. + */ + public static function replace(string $target, string $html, bool $morph = false): string + { + return self::wrap('replace', $target, $html, $morph ? ' method="morph"' : ''); + } + + /** + * Updates the content of the element(s) designated by the target CSS selector. + */ + public static function update(string $target, string $html, bool $morph = false): string + { + return self::wrap('update', $target, $html, $morph ? ' method="morph"' : ''); + } + + /** + * Removes the element(s) designated by the target CSS selector. + */ + public static function remove(string $target): string + { + return \sprintf('', htmlspecialchars($target)); + } + + /** + * Inserts before the element(s) designated by the target CSS selector. + */ + public static function before(string $target, string $html): string + { + return self::wrap('before', $target, $html); + } + + /** + * Inserts after the element(s) designated by the target CSS selector. + */ + public static function after(string $target, string $html): string + { + return self::wrap('after', $target, $html); + } + + /** + * Initiates a Page Refresh to render new content with morphing. + * + * @see Initiates a Page Refresh to render new content with morphing. + */ + public static function refresh(?string $requestId = null): string + { + if (null === $requestId) { + return ''; + } + + return \sprintf('', htmlspecialchars($requestId)); + } + + private static function wrap(string $action, string $target, string $html, string $attr = ''): string + { + return \sprintf(<< + + + EOHTML, $action, htmlspecialchars($target), $attr, $html); + } +} diff --git a/src/Turbo/src/TurboStreamResponse.php b/src/Turbo/src/TurboStreamResponse.php new file mode 100644 index 00000000000..e597ed30775 --- /dev/null +++ b/src/Turbo/src/TurboStreamResponse.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Turbo; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\UX\Turbo\Helper\TurboStream; + +class TurboStreamResponse extends Response +{ + public function __construct(?string $content = '', int $status = 200, array $headers = []) + { + parent::__construct($content, $status, $headers); + + if (!$this->headers->has('Content-Type')) { + $this->headers->set('Content-Type', TurboBundle::STREAM_MEDIA_TYPE); + } + } + + /** + * @return $this + */ + public function append(string $target, string $html): static + { + $this->setContent($this->getContent().TurboStream::append($target, $html)); + + return $this; + } + + /** + * @return $this + */ + public function prepend(string $target, string $html): static + { + $this->setContent($this->getContent().TurboStream::prepend($target, $html)); + + return $this; + } + + /** + * @return $this + */ + public function replace(string $target, string $html, bool $morph = false): static + { + $this->setContent($this->getContent().TurboStream::replace($target, $html, $morph)); + + return $this; + } + + /** + * @return $this + */ + public function update(string $target, string $html, bool $morph = false): static + { + $this->setContent($this->getContent().TurboStream::update($target, $html, $morph)); + + return $this; + } + + /** + * @return $this + */ + public function remove(string $target): static + { + $this->setContent($this->getContent().TurboStream::remove($target)); + + return $this; + } + + /** + * @return $this + */ + public function before(string $target, string $html): static + { + $this->setContent($this->getContent().TurboStream::before($target, $html)); + + return $this; + } + + /** + * @return $this + */ + public function after(string $target, string $html): static + { + $this->setContent($this->getContent().TurboStream::after($target, $html)); + + return $this; + } + + /** + * @return $this + */ + public function refresh(?string $requestId = null): static + { + $this->setContent($this->getContent().TurboStream::refresh($requestId)); + + return $this; + } +} diff --git a/src/Turbo/tests/Helper/TurboStreamTest.php b/src/Turbo/tests/Helper/TurboStreamTest.php new file mode 100644 index 00000000000..263e454effd --- /dev/null +++ b/src/Turbo/tests/Helper/TurboStreamTest.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Turbo\Tests\Helper; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Turbo\Helper\TurboStream; + +class TurboStreamTest extends TestCase +{ + /** + * @testWith ["append"] + * ["prepend"] + * ["replace"] + * ["update"] + * ["before"] + * ["after"] + */ + public function testStream(string $action): void + { + $this->assertSame(<< + + + EOHTML, + TurboStream::$action('some["selector"]', '
content
') + ); + } + + /** + * @testWith ["replace"] + * ["update"] + */ + public function testStreamMorph(string $action): void + { + $this->assertSame(<< + + + EOHTML, + TurboStream::$action('some["selector"]', '
content
', morph: true) + ); + } + + public function testRemove(): void + { + $this->assertSame(<< + EOHTML, + TurboStream::remove('some["selector"]') + ); + } + + public function testRefreshWithoutId(): void + { + $this->assertSame(<< + EOHTML, + TurboStream::refresh() + ); + } + + public function testRefreshWithId(): void + { + $this->assertSame(<< + EOHTML, + TurboStream::refresh('a"b') + ); + } +} diff --git a/src/Turbo/tests/app/Kernel.php b/src/Turbo/tests/app/Kernel.php index 078ba8da579..f61de905272 100644 --- a/src/Turbo/tests/app/Kernel.php +++ b/src/Turbo/tests/app/Kernel.php @@ -220,7 +220,7 @@ public function songs(Request $request, EntityManagerInterface $doctrine, Enviro $song->artist = $doctrine->find(Artist::class, $artistId); } } - if ($remove = $request->get('remove')) { + if ($request->get('remove')) { $doctrine->remove($song); } else { $doctrine->persist($song);