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 SubscribeThe 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!
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.
"Houston: no signs of life"
Start the conversation!
// composer.json
{
"require": {
"php": ">=8.1",
"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.18.5
"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
}
}
// 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
}
}