Skip to content

Quick start

This page wires both halves in a Symfony application with the bundle registered (see Installation).

In your JavaScript entry point, give TurboHandler the functions that set up and tear down your widgets (tooltips, selects, date pickers…) inside a container:

// assets/app.js (assets/app.ts with TypeScript)
import '@hotwired/turbo'
import { TurboHandler } from '@silarhi/turbo'
const handler = new TurboHandler({
onMount: (container) => app.mount(container),
onUnmount: (container) => app.unmount(container),
})
handler.start() // attach listeners (idempotent)
// handler.stop() // detach them all and reset

app.mount() and app.unmount() stand for your own code. TurboHandler decides when and on which element they run: the <body> on the first page load and after each Drive render, a frame after it loads, the nodes a Turbo Stream inserted or removed. Details: TurboHandler.

Without the bundle, a form inside a <turbo-frame> that redirects after a successful submit makes Turbo load the redirect target inside the frame. With the bundle and the handler, the redirect becomes a full page visit:

<turbo-frame id="project-edit">
{{ form(form) }}
</turbo-frame>
#[Route('/projects/{id}/edit', name: 'project_edit')]
public function edit(Project $project, Request $request): Response
{
// ... handle the form
if ($form->isSubmitted() && $form->isValid()) {
return $this->redirectToRoute('project_show', ['id' => $project->getId()]);
}
return $this->render('project/edit.html.twig', ['form' => $form]);
}
  1. FramePOST /projects/1/edit
  2. Controller302, Location: /projects/1
  3. TurboFrameListener204, Turbo-Location: /projects/1
  4. TurboHandlerTurbo.visit('/projects/1')

The controller stays unaware of Turbo. To keep a frame’s redirects inside it, add data-turbo-follow-redirect to the frame: see Keeping a redirect in its frame.

With the turbo_frame filter, the same action renders the full page on a normal request and a lean layout when a frame asks for it:

{# templates/project/edit.html.twig #}
{% extends 'base.html.twig'|turbo_frame('project-edit') %}

Inside the project-edit frame, the template extends base-frame.html.twig instead of base.html.twig: the filter looks for the -frame sibling of the template first, then falls back to the configured base_template (also base-frame.html.twig by default). Create that layout, or frame requests fail with “Unable to find template”. It needs a <turbo-frame> with the id of the requesting frame, and the blocks your pages fill:

{# templates/base-frame.html.twig #}
<turbo-frame id="{{ app.request.headers.get('Turbo-Frame') }}">
{% block body %}{% endblock %}
</turbo-frame>

See The turbo_frame filter.