Skip to content

[Turbo] Add Helper/TurboStream::append() et al. methods #2196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Turbo/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Turbo/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/Turbo/phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -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
97 changes: 97 additions & 0 deletions src/Turbo/src/Helper/TurboStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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('<turbo-stream action="remove" targets="%s"></turbo-stream>', 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 '<turbo-stream action="refresh"></turbo-stream>';
}

return \sprintf('<turbo-stream action="refresh" request-id="%s"></turbo-stream>', htmlspecialchars($requestId));
}

private static function wrap(string $action, string $target, string $html, string $attr = ''): string
{
return \sprintf(<<<EOHTML
<turbo-stream action="%s" targets="%s"%s>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note the "s" to targets, and the docblock comments: we support only CSS selectors, not pure IDs (CSS selectors are more capable anyway).

<template>%s</template>
</turbo-stream>
EOHTML, $action, htmlspecialchars($target), $attr, $html);
}
}
107 changes: 107 additions & 0 deletions src/Turbo/src/TurboStreamResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
}
}
79 changes: 79 additions & 0 deletions src/Turbo/tests/Helper/TurboStreamTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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
<turbo-stream action="{$action}" targets="some[&quot;selector&quot;]">
<template><div>content</div></template>
</turbo-stream>
EOHTML,
TurboStream::$action('some["selector"]', '<div>content</div>')
);
}

/**
* @testWith ["replace"]
* ["update"]
*/
public function testStreamMorph(string $action): void
{
$this->assertSame(<<<EOHTML
<turbo-stream action="{$action}" targets="some[&quot;selector&quot;]" method="morph">
<template><div>content</div></template>
</turbo-stream>
EOHTML,
TurboStream::$action('some["selector"]', '<div>content</div>', morph: true)
);
}

public function testRemove(): void
{
$this->assertSame(<<<EOHTML
<turbo-stream action="remove" targets="some[&quot;selector&quot;]"></turbo-stream>
EOHTML,
TurboStream::remove('some["selector"]')
);
}

public function testRefreshWithoutId(): void
{
$this->assertSame(<<<EOHTML
<turbo-stream action="refresh"></turbo-stream>
EOHTML,
TurboStream::refresh()
);
}

public function testRefreshWithId(): void
{
$this->assertSame(<<<EOHTML
<turbo-stream action="refresh" request-id="a&quot;b"></turbo-stream>
EOHTML,
TurboStream::refresh('a"b')
);
}
}
2 changes: 1 addition & 1 deletion src/Turbo/tests/app/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down