Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Form Submits & The Preview Feature

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

One of the cooler features of Turbo Drive is its snapshot feature, which we know about already. When we visit a page that we've already been to, like Office Supplies or Furniture, it instantly shows the snapshot while it waits for the new Ajax call to finish in the background. And when we hit back, it instantly shows the snapshot with no Ajax call.

This feature, which is great for making your site feel really snappy - is, I'll admit, one of the most problematic when it comes to perfecting your site with Turbo Drive.

Snapshots and Form Submits

Let's see one problem. Head over to the registration page and fill out the form incorrectly: I'll use a bogus email address and hit enter. Cool. The form submitted via Ajax and we see the errors.

Now click back to the homepage. I'm going to revisit the registration page. But watch closely when I do. Woh! For just a moment, we saw the form with the values filled in and the validation errors!

Here's what happened. When we were originally on the registration page with the validation errors showing, we clicked to leave the page to go to the homepage. At that moment - just before we were navigated away, Turbo saved the snapshot for the registration page. That means the snapshot was for a page that had filled-in form fields and validation errors.

Then, when we clicked back to the registration page, that snapshot was restored with errors and all. A moment later, when the Ajax call finished, the fresh content - with an empty form - replaced the snapshot.

This is a known issue with submitted forms. And... well... maybe it's not really an issue. It's... tricky. And maybe you don't really care that this shows up for a moment before it clears. In that case, just ignore it and move on with your life! Go grab a baguette!

How to Handle Problematic Snapshots

But let's say that we do want to avoid this. One option is that we could disable the snapshot from being taken on this page completely. But when I fill out the form... and get the errors... and go to the homepage... and then hit the "back" button in my browser, it is nice that, thanks to the snapshot, we see the form with the fields still filled-in. So... you kind of want the snapshot cache to be used when hitting the back button... but not for the preview.

There are two main ways to fix the problem of a "bad snapshot". The first involves preparing a page before its snapshot is taken. We could clear the form errors and empty the fields so that the snapshot is clean. The code to do this would work for any form on your site... so it would kind of take care of everything all at once. The only downside is that clicking the back button would show an empty form. We're not going to use this solution in this case, but we will leverage this soon for a different preview problem.

Disabling the Preview on a Page

A second solution is to simply disable the preview feature for this page. And, that's one of the nice things about Turbo. Don't like something? Just disable it.

How? By adding a special meta tag to the head element. Head over to the code and open up templates/base.html.twig. We don't want to remove the preview functionality for every page. So instead of adding the meta tag right here, add a block so that a child template can add new meta elements: {% block metas %} {% endblock %}.

... lines 1 - 2
<meta charset="UTF-8">
{% block metas %}{% endblock %}
<title>{% block title %}MVP Office Supplies{% endblock %}</title>
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
... lines 15 - 85

Now open up templates/registration/register.html.twig and override that block: {% block metas %}, {% endblock %} and inside add <meta> name="turbo-cache-control" with content="no-preview".

... lines 1 - 4
{% block metas %}
<meta name="turbo-cache-control" content="no-preview">
{% endblock %}
... lines 8 - 36

The no-preview means: don't show a preview for this page. The other possible value is no-cache, which tells Turbo to not do any snapshotting: not even for the back button.

Let's see how this feels! Refresh the registration page, fill out the form with errors and click away from this page. Now, click back to it. Beautiful! Instead of instantly showing the preview, it stayed on the previous page until the new Ajax call finished loading, just like a normal navigation. You can repeat this for any pages that have a public-facing form where you care enough to avoid this problem.

Dimming the Opacity of a Preview

Speaking of the preview feature, you can also change what a preview looks like... in case you want to make it more obvious that a preview is being shown or give it a "loading" feel. How? Open your Elements inspector. It's quick, but watch this html element. Whenever you navigate and a preview is rendered, Turbo will add a data-turbo-preview attribute to the html element.

Boom! It was fast, but I saw it! Let's use that to see if we can lower the opacity on previews.

Head over to assets/styles/app.css. Target that attribute using the lesser-known attribute syntax: [data-turbo-preview] then body to apply some body styling. Set the opacity to .2 so it's really obvious.

... lines 1 - 7
[data-turbo-preview] body {
opacity: .2;
... lines 11 - 167

Let's go check it! Refresh. As we click to new pages, we don't see anything. But if we click to a page that we've been to... yes! The whole page was nearly invisible while the preview was being shown. This is also kind of a fun way, while you're developing, to get a feel for when a preview is shown.

But... since this looks a bit extreme, let's go back to app.css and comment it out.

... lines 1 - 7
[data-turbo-preview] body {
opacity: .2;
... lines 13 - 169

Next: in addition to the form situation we just saw, there's one other common time when the preview feature will do something that... we don't want. Let's talk about what happens when something like a modal is open at the moment a snapshot is taken.

Leave a comment!

Login or Register to join the conversation
mofogasy Avatar
mofogasy Avatar mofogasy | posted 1 year ago | edited

Hi, my project is not really related to this course project but maybe you have an easy solution.
I use embedded controller to render 2 twigs on the same page :
the first twig renders every review
the second twig renders the form to submit a review. This is done with {{ render(path('app_new_review',{id:product.id})) }}
<br />#[Route('/newreview/{id}', name: "app_new_review")] public function newreview(Product $product,Request $request,EntityManagerInterface $entityManager):Response { $avis = new Review(); ... $form = $this->createForm(AvisType::class,$avis); $form->handleRequest($request); if ($form->isSubmitted() && $form-> isValid()){ $entityManager->persist($avis); $entityManager->flush(); return $this->redirectToRoute('app_avis',['id'=>$product->getId()]); } return $this->renderForm('review/_form.html.twig', [ 'reviewform' => $form, ]); }<br />
is correct. I tried it on an independant page and it works.

But when I put with "all reviews" twig, it won't submit.
Actually, I tried dd("foo"); in the newreview function but it seems that the function isn't executed.

I d like to keep the embedded system so that my "indexreview" function and my allreviews twig are not overloaded.

Can you help me figure out how to use/submit a form included in twig with render(path()) ?


Hey Grenouille,

Well, first of all I would not recommend you using that "render(path())" feature unless you know that you really need it :) If you profile the page, you will notice that those are heavy calls because they send sub-requests to the Symfony app. That feature is mostly use when you use ESI caching strategy, i.e. when you HTTP cache the whole page but those modules you are inserted will be more dynamic on that page. And even if you use ESI caching - I would recommend you to render all the reviews in all "render(path())" instead of rendering this way each review, it would be better for performance.

So, in general, unless you use that ESI caching - I'd suggest you to rewrite that in a different way with a better performance. You can. still hold the logic in different templates, and use {{ include('') }} Twig function to render them, but you would need to pass all the required variables to it. You can pass them directly from the controller, or create a Twig function that will return some necessary data directly in those templates, something like {% set reviews = getAllReviewsForTheCurrentPage() %} and then use that {{ reviews }} var in the template. The same with the form, either pass it directly form the controller or create a Twig function that will create and return that form in your template.

What about form, please, make sure you set the "action" attribute explicitly on that form, otherwise the form will be submitted to the same page you're on.

I hope this helps!


mofogasy Avatar

Hi okay, thanks.
Changed it as you advised


Hey Grenouille,

Awesome! Yeah, that render(path()) feature is great but you should know what you're doing, in other case it would cause more trouble than help :)


Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "", //
        "doctrine/annotations": "^1.0", // 1.13.1
        "doctrine/doctrine-bundle": "^2.2", // 2.3.2
        "doctrine/orm": "^2.8", // 2.9.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^6.1", // v6.1.4
        "symfony/asset": "5.3.*", // v5.3.0-RC1
        "symfony/console": "5.3.*", // v5.3.0-RC1
        "symfony/dotenv": "5.3.*", // v5.3.0-RC1
        "symfony/flex": "^1.3.1", // v1.18.5
        "symfony/form": "5.3.*", // v5.3.0-RC1
        "symfony/framework-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/property-access": "5.3.*", // v5.3.0-RC1
        "symfony/property-info": "5.3.*", // v5.3.0-RC1
        "symfony/proxy-manager-bridge": "5.3.*", // v5.3.0-RC1
        "symfony/runtime": "5.3.*", // v5.3.0-RC1
        "symfony/security-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/serializer": "5.3.*", // v5.3.0-RC1
        "symfony/twig-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/ux-chartjs": "^1.1", // v1.3.0
        "symfony/ux-turbo": "^1.3", // v1.3.0
        "symfony/ux-turbo-mercure": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.0-RC1
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.2
        "symfony/yaml": "5.3.*", // v5.3.0-RC1
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.1
        "twig/intl-extra": "^3.2", // v3.3.0
        "twig/string-extra": "^3.3", // v3.3.1
        "twig/twig": "^2.12|^3.0" // v3.3.2
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.3.0-RC1
        "symfony/maker-bundle": "^1.27", // v1.31.1
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/stopwatch": "^5.2", // v5.3.0-RC1
        "symfony/var-dumper": "^5.2", // v5.3.0-RC1
        "symfony/web-profiler-bundle": "^5.2", // v5.3.0-RC1
        "zenstruck/foundry": "^1.10" // v1.10.0

What JavaScript libraries does this tutorial use?

// package.json
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.13.13
        "@fortawesome/fontawesome-free": "^5.15.3", // 5.15.3
        "@hotwired/turbo": "^7.0.0-beta.5", // 1.2.6
        "@popperjs/core": "^2.9.1", // 2.9.2
        "@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
        "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
        "@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/Resources/assets", // 0.1.0
        "@symfony/ux-turbo-mercure": "file:vendor/symfony/ux-turbo-mercure/Resources/assets", // 0.1.0
        "@symfony/webpack-encore": "^1.0.0", // 1.3.0
        "bootstrap": "^5.0.0-beta2", // 5.0.1
        "chart.js": "^2.9.4",
        "core-js": "^3.0.0", // 3.13.0
        "jquery": "^3.6.0", // 3.6.0
        "react": "^17.0.1", // 17.0.2
        "react-dom": "^17.0.1", // 17.0.2
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "stimulus": "^2.0.0", // 2.0.0
        "stimulus-autocomplete": "https://github.com/weaverryan/stimulus-autocomplete#toggle-event-always-dist", // 2.0.0
        "stimulus-use": "^0.24.0-1", // 0.24.0-2
        "sweetalert2": "^11.0.8", // 11.0.12
        "webpack-bundle-analyzer": "^4.4.0", // 4.4.2
        "webpack-notifier": "^1.6.0" // 1.13.0