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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWhen 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.
Okay, I need help with this.
Here's the use case: Two column page.
Col-8 on the left is a table, with a toolbar with things like a date input, and filters that apply to the table. The table is meant to pull in a single day's data from the db, and said filters can be applied to the results.
Col-4 on the left is a reporting section. It displays a totals table, and a chart. The filters don't apply to this section, but the date input does.
I want this col-4 section to load separately, because it doesn't always need to update, and I also want to break up a complicated page. I thought "hey, turbo frame or streams, right?"
Here's the stream template (I removed the template code, there is something there in the actual file).
That action listens for date changes. The stimulus controller looks like this, and this is the part I'm not sure at all about:
This is the PHP controller:
The stimulus controller definitely logs the html from the stream template on connect, AND when the reload is triggered. But the html on the page doesn't change. Is this the wrong approach? Or am I missing some obvious step or something?
I did also try updating the html directly in the stimulus controller, by using
$('#scan-stats-stream').html(response.data), and that doesn't work, either. I can see it in the console, but it's a grayed-out "document-fragment." It won't display. I can even dig into it, and I definitely see the table and the chart's code. So it's definitely returning the template, and even putting in in the right location, but it won't actually display.The only solution I came up with is to remove the <template> tags around the stream. That makes it work, but I think it's because I'm working around how it's supposed to work. As in, the action="replace" isn't doing anything at all, since I'm just updating it in stimulus.
How do I get this to work as intended, with an on-demand post request?