Streams: Reusing Templates

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

When we submit the product review form, instead of redirecting like we were before, we're now returning this TurboStreamResponse. When the Ajax call finishes, Turbo notices that we're returning this type of response... instead of a real HTML page. And so, instead of handling the HTML like a frame normally would, it passes it to the Turbo Stream system.

Right now, we're using it to update the quick stats area of the page with this random HTML. If you refresh, the real goal is to update the review count and review average as soon as the new review is submitted.

Reusing Templates in a Stream

To do that, without repeating ourselves, over in show.html.twig - the template for the product show page - copy the quick stats code... and create a new template in templates/product/ called, how about _quickStats.html.twig. Paste the code here.

<strong>{{ product.priceString|format_currency('USD') }}</strong>
<br>
<strong>{{ product.reviews|length }}</strong> Reviews
<br/>
<strong>{{ product.averageStars }}/5</strong><i class="fas fa-star ms-2"></i>

We can now reuse this in two places. In show.html.twig include it: product/_quickStats.html.twig

{% extends 'product/productBase.html.twig' %}
{% block productBody %}
<turbo-frame id="product-info" target="_top" class="row pt-3 product-show">
... lines 5 - 31
<div class="p-3 mt-4 d-flex justify-content-between flex-wrap flex-lg-nowrap">
<div id="product-quick-stats">
{{ include('product/_quickStats.html.twig') }}
</div>
<div>
{{ include('product/_cart_add_controls.html.twig') }}
</div>
</div>
... line 40
</turbo-frame>
... lines 42 - 46
{{ include('product/_reviews.html.twig') }}
{% endblock %}

Then, in the stream template, do the same thing!

<turbo-stream action="update" target="product-quick-stats">
<template>
{{ include('product/_quickStats.html.twig') }}
</template>
</turbo-stream>

Pretty cool, right?

Let's try that. Refresh. This still works and shows 10 reviews. Scroll down and add review number 11. Submit and... oh! The entire reviews section is gone! My web debug toolbar is angry: that Ajax call returned a 500 error.

Open up its profiler.

Variable product does not exist

Coming from _quickStats.html.twig. Of course, the problem is that we're including _quickStats.html.twig from reviews.stream.html.twig. In ProductController, we're not passing any variables to that template... but in quick stats, we need a product!

No problem: pass product set to $product, which will make it available all the way into the quick stats template.

... lines 1 - 18
class ProductController extends AbstractController
{
... lines 21 - 69
/**
* @Route("/product/{id}/reviews", name="app_product_reviews")
*/
public function productReviews(Product $product, CategoryRepository $categoryRepository, Request $request, EntityManagerInterface $entityManager)
{
... lines 75 - 85
if ($reviewForm->isSubmitted() && $reviewForm->isValid()) {
... lines 87 - 89
return $this->render('product/reviews.stream.html.twig', [
'product' => $product,
], new TurboStreamResponse());
... lines 93 - 98
}
... lines 100 - 107
}
... lines 109 - 116
}

Okay: take two. Refresh again. We now have 11 reviews... so let's go add number 12. Submit. The reviews section is still weird - but that's ok. Scroll up. Yes! Our Turbo Stream updated the area with the real data! That is so cool!

Return a Stream or HTML, not both (yet)

Now we need to fix the reviews area... because showing a filled-in form with a disabled button... doesn't exactly scream "the review was successfully saved".

The entire reviews area lives in templates/product/_reviews.html.twig... and all of it is surrounded by the product-review frame. So both the list of reviews and the form are in this frame.

Thanks to this, before we started messing around with turbo streams, after submit, we redirected to the reviews page. That page includes this template with this frame. And so, the entire frame updated, including the new review and a fresh, empty form.

At this point, we have two choices. First, we could redirect on success like we were doing before and let the normal turbo frame logic do its magic. Or we can return a turbo stream and update whatever elements we want. But, we can't do both. Our controller can only return one thing, so we need to choose between returning a redirect or returning a stream.

Well, actually we can do both... but let's keep that a secret between you and I for now. It's a topic for later... and requires one extra piece of technology.

So... what can we do? Well, if we want to be able to update the quick stats area and the reviews area, the answer is to return a stream that contains multiple instructions. Let's see how next.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=7.4.0",
        "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.13.3
        "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
    }
}