Ajax-Powered HTML Updates & a CSS Transition

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

Time to make the Ajax call. In our new stimulus controller - cart-list - add a value called cartRefreshUrl, which will be a String. We're doing this, like we've done before, to avoid hardcoding the URL to the endpoint.

... lines 1 - 2
export default class extends Controller {
static values = {
cartRefreshUrl: String,
}
... lines 7 - 11
}

Fetching the Fresh HTML

Copy cartRefreshUrl, go to cart.html.twig, add a second argument to stimulus_controller() and set cartRefreshUrl to path() and the name of our route: _app_cart_list.

... lines 1 - 2
{% block body %}
... lines 4 - 13
<div
... line 15
{{ stimulus_controller('cart-list', {
cartRefreshUrl: path('_app_cart_list')
}) }}
>
... line 20
</div>
... lines 22 - 24
{% endblock %}
... lines 26 - 27

Making the Ajax call is probably the easiest part. Down in removeItem(), say const response = await fetch(this.cartRefreshUrlValue). And, of course, as soon as we use await, we need to make the method async. Finish by replacing the entire HTML of this element with the response text: this.element.innerHTML = await response.text().

... lines 1 - 2
export default class extends Controller {
... lines 4 - 7
async removeItem(event) {
const response = await fetch(this.cartRefreshUrlValue);
this.element.innerHTML = await response.text();
}
}

We're done! Testing time. Oh, but an empty cart is no fun... let's add a few more items. And... excellent! Remove the red sofa, confirm and... oh! That was awesome! We get the entire, no-full-page-refresh experience with zero duplication and minimal JavaScript. I mean, check out how big the controller is! It's teenie tiny!

Stimulus Re-Initialized on the new HTML

And a super important, amazing thing just happened automatically. We add new HTML to the page. In fact, all of the HTML inside of this element is brand new. Normally, with JavaScript, that's a problem: any event listeners that we need on the elements - like a submit listener that opens a dialog - need to be manually reattached to the new elements.

But with Stimulus, it all... just works! We talked about this earlier. As soon as Stimulus saw these two new data-controller="submit-confirm" elements on the page, it instantiated two fresh new submit-confirm controllers. And everything behaves perfectly. Watch: if we click remove... that still works! We don't need to think about anything.

A Simple CSS Transition

I'm so excited about this that I want to add one last tiny extra detail to make it really smooth. I want to make the row fade out before it disappears. We can do this with a CSS transition.

Open up assets/styles/app.css and scroll down a bit: I'm looking for cart-item. Here it is. This is the class that's around each cart row. Add transition: opacity 500ms

... lines 1 - 86
.cart .cart-item {
... line 88
transition: opacity 500ms;
}
... lines 91 - 150

That doesn't actually make it transition. This just says: if the opacity ever changes... for any reason, I want you to change the opacity gradually to the new value over 500 milliseconds.

Below this, add another .cart .cart-item with .removing and set opacity: 0.

... lines 1 - 91
.cart .cart-item.removing {
opacity: 0;
}
... lines 95 - 150

This says, if the cart-item element also has a removing class, change the opacity to zero. Thanks to the transition, that change will happen gradually.

And where does this removing class come from? Good question! We are going to add it.

Back in the controller, right at the beginning, add event.currentTarget. That will get us the element that's around the entire row: this element here... which has the cart-item class on it. Then .classList.add('removing').

... lines 1 - 2
export default class extends Controller {
... lines 4 - 7
async removeItem(event) {
event.currentTarget.classList.add('removing');
... lines 10 - 12
}
}

Try it! Refresh. Let's delete the blue sofa. Watch closely. Yes! It was quick, but it faded out before it was replaced. Remove the last one. That's so cool.

If your server is super fast, the fading out might not finish before the HTML reloads. If you care enough, you could delay the Ajax call a few milliseconds with setTimeout() or get super fancy with some extra promises.

Later, we'll talk about how to add CSS transitions in a different, more robust way. But this was easy and works nicely!

Next, we've talked a lot about Stimulus. But isn't this also a tutorial about Symfony UX? What is that? And how does it fit in? Let's find out by adding a JavaScript-powered chart to our page... by only writing PHP code.

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.11.1
        "doctrine/doctrine-bundle": "^2.2", // 2.2.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.8", // 2.8.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^5.6", // v5.6.1
        "symfony/asset": "5.2.*", // v5.2.3
        "symfony/console": "5.2.*", // v5.2.3
        "symfony/dotenv": "5.2.*", // v5.2.3
        "symfony/flex": "^1.3.1", // v1.12.1
        "symfony/form": "5.2.*", // v5.2.3
        "symfony/framework-bundle": "5.2.*", // v5.2.3
        "symfony/property-access": "5.2.*", // v5.2.3
        "symfony/property-info": "5.2.*", // v5.2.3
        "symfony/proxy-manager-bridge": "5.2.*", // v5.2.3
        "symfony/security-bundle": "5.2.*", // v5.2.3
        "symfony/serializer": "5.2.*", // v5.2.3
        "symfony/twig-bundle": "5.2.*", // v5.2.3
        "symfony/ux-chartjs": "^1.1", // v1.2.0
        "symfony/validator": "5.2.*", // v5.2.3
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.1
        "symfony/yaml": "5.2.*", // v5.2.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.2.1
        "twig/intl-extra": "^3.2", // v3.2.1
        "twig/twig": "^2.12|^3.0" // v3.2.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.2.3
        "symfony/maker-bundle": "^1.27", // v1.30.0
        "symfony/monolog-bundle": "^3.0", // v3.6.0
        "symfony/stopwatch": "^5.2", // v5.2.3
        "symfony/var-dumper": "^5.2", // v5.2.3
        "symfony/web-profiler-bundle": "^5.2" // v5.2.3
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.12.13
        "@popperjs/core": "^2.9.1", // 2.9.1
        "@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
        "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
        "@symfony/webpack-encore": "^1.0.0", // 1.0.4
        "bootstrap": "^5.0.0-beta2", // 5.0.0-beta2
        "core-js": "^3.0.0", // 3.8.3
        "jquery": "^3.6.0", // 3.6.0
        "react": "^17.0.1", // 17.0.1
        "react-dom": "^17.0.1", // 17.0.1
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "stimulus": "^2.0.0", // 2.0.0
        "stimulus-autocomplete": "^2.0.1-phylor-6095f2a9", // 2.0.1-phylor-6095f2a9
        "stimulus-use": "^0.24.0-1", // 0.24.0-1
        "sweetalert2": "^10.13.0", // 10.14.0
        "webpack-bundle-analyzer": "^4.4.0", // 4.4.0
        "webpack-notifier": "^1.6.0" // 1.13.0
    }
}