If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOn page load, Turbo did notice our new <turbo-frame>
element and it did make an Ajax request to fetch the contents. But then, for some reason, it gave us this error. Why?
This is a super important detail of Turbo frames. When a frame makes an Ajax call, it looks in the response for a <turbo-frame>
element that has the same id as itself and uses its content only. If it does not find a matching <turbo-frame>
, in the response, then you get this error.
Ok, but... why? If you look in the network tools, the response from the Ajax call contains the exact HTML we want. Why doesn't it just take the entire HTML from the response and put it into the frame?
Well, we're not leveraging it in this example, but one of the super powers of the frame system is that you can point a frame at a URL that returns an entire, full HTML page. So if you pretend that this returns a full HTML page, the frame system is smart enough to only find and use the matching frame. This allows you to create full, normal pages and then reuse those full normal pages to power your frames.. avoiding the need to create extra endpoints for your frames like we did. If this doesn't make sense yet, don't worry. Our next example will illustrate this.
Anyways, what we need to do is make sure that the response contains a <turbo-frame>
element with id="cart-sidebar"
. I'll copy that from cart.html.twig
, open _featuredSidebar.html.twig
, add that... and indent everything.
<turbo-frame id="cart-sidebar"> | |
<div class="component-light product-show p-3 mb-5"> | |
<h5 class="text-center">Featured Product!</h5> | |
<a href="{{ path('app_product', { id: featuredProduct.id }) }}"> | |
<img | |
alt="{{ featuredProduct.name }}" | |
src="{{ asset('/uploads/products/'~featuredProduct.imageFilename) }}" | |
class="d-block" | |
> | |
</a> | |
<div class="pt-3"> | |
<h6 class="d-flex justify-content-between mb-3"> | |
<strong>{{ featuredProduct.name }}</strong> | |
{{ featuredProduct.priceString|format_currency('USD') }} | |
</h6> | |
{{ include('product/_cart_add_controls.html.twig') }} | |
</div> | |
</div> | |
</turbo-frame> |
Notice that we don't have a src=""
on this frame: this is not a lazy frame... it's just a normal frame that already has its final content.
Ok: let's try it again. Refresh and... yes! It works! It looked in the response for the <turbo-frame>
with the id, found it and used its HTML. If you inspect element and find the turbo-frame
, you can see the src=""
attribute is still there, but now it's filled with content.
At this point, if you click any links or submit the form on the sidebar... it might not work like you expect because the frame will keep any navigation inside the frame. That's the first use-case for Turbo Frames - and we'll come back in a few minutes to address this.
Oh, and by the way, if you're using Symfony 5.3 and you create a controller - like this one - that just renders part of a page, you don't have to give this a route. There's another option. Remove this route.
... lines 1 - 29 | |
public function _cartFeaturedProduct(ProductRepository $productRepository): Response | |
{ | |
$featuredProduct = $productRepository->findFeatured(); | |
$addToCartForm = $this->createForm(AddItemToCartFormType::class, null, [ | |
'product' => $featuredProduct, | |
]); | |
return $this->renderForm('cart/_featuredSidebar.html.twig', [ | |
'featuredProduct' => $featuredProduct, | |
'addToCartForm' => $addToCartForm, | |
]); | |
} | |
... lines 43 - 113 |
Now, in cart.html.twig
, instead of {{ path() }}
, use {{ fragment_uri() }}
and then controller()
and then the name of the controller: App\\Controller\\CartController::
and then the method name... which is _featuredProduct
.
... lines 1 - 10 | |
<aside class="col-12 col-md-4 order-2 order-md-1"> | |
<turbo-frame id="cart-sidebar" src="{{ fragment_uri(controller('App\\Controller\\CartController::_cartFeaturedProduct')) }}"> | |
Loading... | |
</turbo-frame> | |
</aside> | |
... lines 16 - 32 |
This is a bit longer - and those double slashes are ugly and needed because backslash is an escape character. Behind the scenes, this will generate a signed URL - called a fragment URL - that renders our controller. To get this to work, make sure that you have the fragment system activated: that's in config/packages/framework.yaml
. Uncomment fragments: true
.
... lines 1 - 14 | |
#esi: true | |
fragments: true | |
php_errors: | |
log: true | |
... lines 19 - 25 |
Let's try this. Move over, refresh the page and cool! It still works! If you look at the turbo-frame
, the src=""
is now set to a long, weird looking _fragments
URL.
Next: let's look at a second lazy frame example. But this time, instead of creating a controller that renders just the frame, we're going to populate a frame by reusing an existing, full HTML page.
Hey Stephane,
I know this error! :) That's because you need to pass a Form View object to the template, for this you need to call "createView()" method on your form object, i.e. "$form->createView()" and pass the result to the template. I bet you missed this createView() call at some point of this tutorial.
Cheers!
Hey Victor,
Thank for your reply. It's ok. Problem solve. I forgot to use $this->renderForm() instead of $this->render() because I delete createView().
Thank for your top tutorial.
Cheers !
PS : the connection to disqu service it is not working with firefox 93.0 on Ubuntu. When I try to log in to click on Disqu icon, the modal appear and disappear immediatly.
Hey Stéphane,
Awesome, thanks for sharing your final solution about this issue :)
P.S. Might be related to the latest Firefox update, I hope it will be fixed soon. Unfortunately, nothing much we can do from our side, we use third-party Disqus code for our comments. Maybe some day we will migrate to a custom comment system instead :) Sorry for any inconveniences with it!
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
"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
}
}
// 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
}
}
Hey,
I have an error when I try to use turbo-frame and nothing appear.
In console, there is error 500.
When I look network view, there is symfony exception :
<br />Symfony\Component\Form\FormRenderer::renderBlock(): Argument #1 ($view) must be of type Symfony\Component\Form\FormView, Symfony\Component\Form\Form given, called in /home/stephane/www/symfonyCats/symfony/sf5/symfony-ux-turbo/var/cache/dev/twig/02/0202569e5340f2a96d80f1a94f082a2dfa2a9a0894b191f9e0278d804571c801.php on line 43<br />
I use Sf 5.3.9