Reusing the "Reload Content" Controller

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

The whole point of our new reusable reload-content_controller is to make an Ajax call and put the HTML from that call into a content target whenever someone calls this refreshContent() method.

We're already using it on our product admin list page. After the new product modal form is submitted successfully, in our template, we listen to the modal-form:success event and trigger the refreshContent method... so that the product list reloads.

What this controller does is super similar to a controller we created earlier: cart-list_controller. In fact, they're basically identical! This is used on the cart page after we remove an item. Let's actually add a couple of items so we can play with this.

Here's the plan: I want to eliminate some custom code by reusing the reload-content_controller here on the cart page. Start with the fun part: deleting cart-list_controller. Bye bye!

Next, open the template for the cart so we can see how that was used: templates/cart/cart.html.twig. Okay, here it is: the cart-list controller is on the div that's around the cart table. Change this to reload-content... and then, the cartRefreshUrl value is now called url in the new controller, so change that here.

... lines 1 - 2
{% block body %}
... lines 4 - 11
<div class="col-xs-12 col-lg-9 cart">
... lines 13 - 15
{{ stimulus_controller('reload-content', {
url: path('_app_cart_list')
}) }}
... lines 19 - 21
</div>
... lines 23 - 24
{% endblock %}
... lines 26 - 27

Great!

Now, we need something to call the refreshContent() method on the controller. How was this working before with the old controller? Let's dive a little deeper into the included _cartList.html.twig template. Let's see. Ah, here it is, I remember now. After we remove an item from the cart - so over here, after we actually hit "yes, remove it" - the submit-confirm controller that handles this dispatches a custom event: submit-confirm:async:submitted. Before, we were listening to that and calling removeItem on the cart-list controller. All we need to do now - since we're using the reload-content controller - is call the refreshContent method.

... lines 1 - 13
{% for item in cart.items %}
<div
... line 16
data-action="submit-confirm:async:submitted->reload-content#refreshContent"
>
... lines 19 - 54
</div>
... lines 56 - 59
{% endfor %}
... lines 61 - 76

Sweet! I think we're done. Testing time!

Making a Target Optional

Head over refresh, remove an item and... uh oh! Let's see:

Error: missing target element reload-content.content

Ah... In reload-content_controller, we put the HTML into a target called content. We forgot to add that target to the cart page! That's kind of an awesome thing about targets: if you create a controller... and that controller requires a target to be defined, you get a pretty clear error if you forget.

Ok: in cart.html.twig, let's think: which element do we need to reload the content into? Actually it's this <div> right here: the same one that has the data-controller attribute on it. So: we should add a new target to this element, right?

We could. But I have a better idea. What if we make the content target optional?

If it is set, we'll put the HTML into it. But if it is not set, we will assume that the HTML should be put into the top level element: this.element.

How can we do that? Check this out. Say const target = this.hasContentTarget. I mentioned this has thing earlier when we first introduced targets, but we haven't really used it yet. This is a safe way to check whether or not there is a content target defined in the HTML. If there is one, then of course we'll use this.contentTarget. Else use this.element.

... lines 1 - 2
export default class extends Controller {
... lines 4 - 8
async refreshContent(event) {
const target = this.hasContentTarget ? this.contentTarget : this.element;
... lines 11 - 15
}
}

Now, down here, use target in those three other places instead of this.contentTarget.

... lines 1 - 8
async refreshContent(event) {
... lines 10 - 11
target.style.opacity = .5;
... line 13
target.innerHTML = await response.text();
target.style.opacity = 1;
}
... lines 17 - 18

Try it now! Refresh, remove an item and... got it! We get the same functionality with less code!

Next: earlier in the tutorial we built a search-preview controller: oooOOOoo. It works really nicely. But... it would look even better with some CSS fade in and fade out transitions. Is that as easy as just... adding and removing a class at the right time in our controller?

In this case... no. But don't worry: we already have a trick up our sleeve that will allow us to add transitions simply and beautifully.

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
    }
}