<turbo-frame id="variable-content">
<p>Prompt for:
<a href="{{ path('my-route') }}">link</a>.
</p>
</turbo-frame>
By default, links inside the frame stay in the frame.
<a href="{{ path('my-route') }}"
data-turbo-frame="variable-content"
>
If you have a form, the action must be with full path. The URL is not updated when loading a frame.
#[Route('/frames/target', name: 'my-route')]
public function target(Request $request): Response
{
$turboFrameId = $request->headers->get('Turbo-Frame');
if ('variable-content' === $turboFrameId) {
// optimization: render only the frame but not the
// rest of the page as it would be discarded anyways.
return $this->render('variable-content.html.twig');
}
return $this->render('full-page.html.twig');
}
<form method="post">
<label for="message">Message</label>
<input id="message" name="message"
type="text" required autofocus>
<button type="submit">Send</button>
</form>
If the backend answers 204 "No Content" to the form submission, Turbo will just stay on the page.
<body
{{ turbo_stream_listen(topic) }}
>
<section id="messages"></section>
Twig\Environment $twig
Symfony\Component\Mercure\HubInterface $hub
#[Route('/streams', name: 'app_streams', methods: ['GET', 'POST'])]
public function index(Request $request): Response
{
if ($request->isMethod('POST')) {
$message = $request->request->get('message', ''));
if ('' !== $message) {
// send message to all connected clients
...
...
// render HTML in backend
$html = $this->twig->render('_message.html.twig', [
'message' => $message,
]);
// add message to the beginning of the list
$payload = TurboStream::prepend('#messages', $html)
// and update this independent section
. TurboStream::update('#last-message',
'Last message received at '.date('H:i:s'))
;
$hub->publish(new Update(self::TOPIC, $payload));
...
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
reset(event) {
if (event.detail.success) {
this.element.reset();
}
}
}
<form
method="post"
data-controller="reset-form"
data-action="turbo:submit-end->reset-form#reset"
>
Or with Twig functions:
<form method="post"
{{ stimulus_controller('reset-form') }}
{{ stimulus_action('reset-form', 'reset', 'turbo:submit-end') }}
>
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['messages'];
static values = {url: String};
connect() {
this.eventSource = new EventSource(this.urlValue);
this.eventSource.onmessage =
(event) => this.appendMessage(event);
}
disconnect() {
this.eventSource?.close();
}
...
...
appendMessage(event) {
const payload = JSON.parse(event.data);
... build HTML element with payload ...
this.messagesTarget.append(article);
this.messagesTarget.scrollTop =
this.messagesTarget.scrollHeight;
}
<div {{ stimulus_controller('subscribe-messages',
{ url: mercure(topic) })
}}>
...
<section class="messages"
{{ stimulus_target('subscribe-messages', 'messages') }}
></section>
$hub->publish(new Update(self::TOPIC, json_encode([
'timestamp' => date('H:i:s'),
'message' => $message,
], \JSON_THROW_ON_ERROR)));
$update = new Update(
'topic-123',
json_encode(['status' => 'OutOfStock']),
private: true,
);
{{ 'eventSrcUrl' : mercure('topic-123', {
subscribe: 'topic-123'
}) }}
new EventSource(this.eventSrcUrlValue, {
withCredentials: true
})
Alternative: HTMX