Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Visually Highlighting new Items that Pop onto the Page

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

Our review system is super cool: if any user submits a review, that review will pop onto the page of anyone else that's currently viewing this product.

To make this a bit more obvious, I want to highlight the new review as soon as it appears. And this is pretty easy. Start over in assets/styles/app.css. Add a .streamed-new-item style with a background-color set to lightgreen.

... lines 1 - 181
.streamed-new-item {
background-color: lightgreen;

Adding a Green Background to New Items

Let's add this class to a new review if it's added via a stream. We can do this in reviews.stream.html.twig: pass a new variable into the template called isNew set to true.

... lines 1 - 6
<turbo-stream action="append" target="product-{{ product.id }}-review-list">
{{ include('product/_review.html.twig', {
review: newReview,
isNew: true
}) }}

Now, over in that template - _review.html.twig - at the end of the class list, use the ternary syntax: if isNew - and default this to false if the variable is not passed in - then print streamed-new-item.

<div class="component-light my-3 p-3{{ isNew|default(false) ? ' streamed-new-item' }}">
<p><i class="fas fa-user-circle me-2"></i>{{ review.owner.email }} <i class="fas fa-star ms-4"></i> {{ review.stars }}/5</p>
{{ review.content }}

That's it. The "else" is automatic: if isNew is false, this will print nothing.

Let's check it out! Refresh both of the pages to get the new CSS... and then submit a new review. Yay! The green background shows up here... and on the page of everyone on the planet that happens to be viewing this page.

So... this is cool. But... we need more fancy! What if we show this background for only five seconds and then fade it out. Start again in app.css to set up the fading out part: we need a new class that describes this transition. Add a fade-background class that declares that we want any background-color changes to happen gradually over 2000 milliseconds.

... lines 1 - 181
.streamed-new-item {
background-color: lightgreen;
.fade-background {
transition: background-color 2000ms;

A Stimulus Controller to Fade Out

Before we try to use this somewhere directly, let's stop and think. If the goal is to remove this background after 5 seconds, then the only way to accomplish that is by writing some custom JavaScript. In other words, we need a Stimulus controller! In the assets/controllers/ directory, create a new file called, how about, streamed-item_controller.js. I'll paste in the normal structure, which imports turbo, exports the controller and creates a connect() method.

import { Controller } from 'stimulus';
export default class extends Controller {
... lines 4 - 6
connect() {
... line 9

Before we fill this in, go over to _review.html.twig and use this. I'll break this onto multiple lines.. cause it's getting kind of ugly. Copy the class name, but delete the custom logic. Replace it with a normal if statement: if isNew|default(false), then we want to activate that new Stimulus controller. Do that with {{ stimulus_controller('streamed-item') }}. Oh, and pass a second argument, I want to pass a variable into the controller called className set to streamed-new-item.

class="component-light my-3 p-3"
{% if isNew|default(false) %}
{{ stimulus_controller('streamed-item', {
className: 'streamed-new-item'
}) }}
{% endif %}
<p><i class="fas fa-user-circle me-2"></i>{{ review.owner.email }} <i class="fas fa-star ms-4"></i> {{ review.stars }}/5</p>
{{ review.content }}

I'm doing this for two reasons. First, it will now be the responsibility of the controller to add this class to the element. We'll do that in a minute. And second, while we don't need it now, making this class name dynamic will help us reuse this controller later.

Anyways, head back to the controller and define the value: static values = {} an object with className which will be a String.

Cool. Down in connect(), add that class to the element: this.element.classList.add() and pass this.classNameValue.

import { Controller } from 'stimulus';
export default class extends Controller {
static values = {
className: String
connect() {

If we stopped right now... this would just be a really fancy way to add the streamed-new-item class to the element as soon as it pops onto the page.

So let's do our real work. Use setTimeout() to wait 5 seconds... and then... if I steal some code... remove this.classNameValue.

If we just did this, after five seconds, the green background would suddenly disappear. To activate the transition when the background is removed, add another class: fade-background.

import { Controller } from 'stimulus';
export default class extends Controller {
static values = {
className: String
connect() {
setTimeout(() => {
}, 5000);

If you wanted to be really fancy, you could wait until the transition finishes and then remove this class to clean things up. But this will work fine.

Let's try it! Refresh both tabs so that we get that new CSS... then go fill in another review. When we submit... good! A green background here... and in the other browser. If we wait... beautiful! It faded out! How nice is that?

Ok team, we're currently publishing updates to Mercure from inside of our controller. But the Mercure Turbo UX package that we installed earlier makes it possible to publish updates automatically whenever an entity is updated, added or removed. It's pretty incredible, and it's our next topic.

Leave a comment!

Login or Register to join the conversation
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