Skip to content

[Turbo] Render TurboStream (wip) #2122

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

Closed
wants to merge 1 commit into from
Closed
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
19 changes: 19 additions & 0 deletions src/TurboStreamRender/src/Core/TurboStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);


namespace src\Core;

use src\Enum\TurboAction;

readonly class TurboStream
{
public function __construct(
public string $target,
public TurboAction $action,
public string $view,
public array $parameters,
) {
}
}
39 changes: 39 additions & 0 deletions src/TurboStreamRender/src/Core/TurboStreamCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);


namespace src\Core;

use src\Enum\TurboAction;

class TurboStreamCollection
{

/**
* @param TurboStream[] $streams
*/
public function __construct(private array $streams = [])
{
}

public function add(string $target, TurboAction $action, $view, array $parameters = [])
{
return $this->addStream(new TurboStream($target, $action, $view, $parameters));
}

public function addStream(TurboStream $stream): self
{
$this->streams[] = $stream;

return $this;
}

/**
* @return TurboStream[]
*/
public function all(): array
{
return $this->streams;
}
}
68 changes: 68 additions & 0 deletions src/TurboStreamRender/src/Core/TurboStreamRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace src\Core;

use src\Enum\TurboAction;
use TurboStreamRendererInterface;

class TurboStreamRenderer implements TurboStreamRendererInterface
{
/**
* This is a hacky way to reuse the code of renderView form AbstractController - just pass the method as a callable to the renderer constructor.
* @var callable $renderer
*/
protected $renderer = null;

public function __construct(callable $renderer)
{
$this->renderer = $renderer;
}

/**
* @throws \InvalidRendererException
*/
public function renderView(string $view, array $parameters = []): string
{
try {
$renderer = $this->renderer;
return $renderer($view, $parameters);
}catch (\Throwable $e) {
throw new \InvalidRendererException($e->getMessage(), $e->getCode(), $e);
}
}

public function renderAsStreamView(string $target, TurboAction $action, string $view, array $parameters = []): string
{
$streams = (new TurboStreamCollection())->add($target, $action, $view, $parameters);

return $this->renderTurboStreamsView($streams->all());
}

/**
* @throws \InvalidRendererException
*/
public function renderTurboStreamsView(array $streams): string
{
$response = "";
foreach ($streams as $stream) {
$content = $this->renderView($stream['view'], $stream['parameters']);
$response .= $this->wrapWithTurboStream($stream['target'], $stream['action'], $content);
$response .= "\n";
}

return $response;
}

private function wrapWithTurboStream(string $target, TurboAction $action, string $content): string
{
return <<<HTML
<turbo-stream action="$action->value" target="$target">
<template>
$content
</template>
</turbo-stream>
HTML;
}
}
17 changes: 17 additions & 0 deletions src/TurboStreamRender/src/Enum/TurboAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace src\Enum;

enum TurboAction: string
{
case APPEND = 'append';
case PREPEND = 'prepend';
case REPLACE = 'replace';
case UPDATE = 'update';
case REMOVE = 'remove';
case BEFORE = 'before';
case AFTER = 'after';
case REFRESH = 'refresh';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

declare(strict_types=1);


class InvalidRendererException extends \Exception
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

use src\Core\TurboStream;
use src\Enum\TurboAction;

interface TurboStreamRendererInterface
{
function renderAsStreamView(string $target, TurboAction $action, string$view, array $parameters = []): string;

/**
* @param TurboStream[] $streams
*/
function renderTurboStreamsView(array $streams): string;
}
45 changes: 45 additions & 0 deletions src/TurboStreamRender/src/Trait/TurboRenderTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace src\Trait;

use src\Core\TurboStream;
use src\Core\TurboStreamCollection;
use src\Enum\TurboAction;
use TurboStreamRendererInterface;

// This needs Symfony\Component\HttpFoundation\Response so it won't work as part of the turbo bundle, unless there is another way to keep it decoupled.
// An alternative would be to have this as a separate bundle.


trait TurboRenderTrait
{
abstract protected function getTurboStreamRenderer(): TurboStreamRendererInterface;

protected function renderAsStreamView(string $target, TurboAction $action, $view, array $parameters = []): string
{
$streams = (new TurboStreamCollection())->add($target, $action, $view, $parameters);

return $this->renderTurboStreamsView($streams->all());
}

/**
* @param TurboStream[] $streams
*/
protected function renderTurboStreamsView(array $streams): string
{
return $this->getTurboStreamRenderer()->renderTurboStreamsView($streams);
}

private function wrapWithTurboStream(string $target, TurboAction $action, string $content): string
{
return <<<HTML
<turbo-stream action="$action->value" target="$target">
<template>
$content
</template>
</turbo-stream>
HTML;
}
}
66 changes: 66 additions & 0 deletions src/TurboStreamResponse/src/Trait/TurboResponseTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace src\Trait;

use src\Core\TurboStream;
use src\Enum\TurboAction;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\UX\Turbo\TurboBundle;

// This needs Symfony\Component\HttpFoundation\Response so it won't work as part of the turbo bundle, unless there is another way to keep it decoupled.
// An alternative would be to have this as a separate bundle.


trait TurboResponseTrait
{

use TurboRenderTrait;

protected function renderAsStream(string $target, TurboAction $action, $view, array $parameters = []): Response
{
return $this->renderTurboStreams([new TurboStream($target, $action, $view, $parameters)]);
}

/**
* @param TurboStream[] $streams
*/
protected function renderTurboStreams(array $streams): Response
{
$content = $this->renderTurboStreamsView($streams);
$response ??= new Response();

// I would like to keep this part of the standard AbstractController, but I don't really like the idea of duplication the code.
// Any suggestions for a way to reuse it?

foreach ($streams as $stream) {
if (200 === $response->getStatusCode()) {
$parameters = $stream->getParameters();
foreach ($parameters as $v) {
if ($v instanceof FormInterface && $v->isSubmitted() && !$v->isValid()) {
$response->setStatusCode(422);
break 2;
}
}
}
}

$response->setContent($content);
$response->headers->set('Content-Type', TurboBundle::STREAM_MEDIA_TYPE);

return $response;
}

private function wrapWithTurboStream(string $target, TurboAction $action, string $content): string
{
return <<<HTML
<turbo-stream action="$action->value" target="$target">
<template>
$content
</template>
</turbo-stream>
HTML;
}
}
5 changes: 5 additions & 0 deletions ux.symfony.com/templates/components/TurboStream.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<turbo-stream action="{{ action }}" target="{{ target }}">
<template>
{% block content %}{% endblock %}
</template>
</turbo-stream>