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 SubscribeEarlier, when we were working on the cart page, we learned that if you need two controllers to talk to each other, like the modal-form_controller
and the reload-content_controller
, a great way to do that is to dispatch an event. Specifically, reload-content_controller
needs to know when it should refresh the content. To help it know that, we're going to dispatch a custom event from modal-form
after the form submits successfully.
So since we did this before, let's jump straight in. First, at the top, import { useDispatch } from 'stimulus-use'
. Then activate that with a new connect()
method: useDispatch(this)
. Temporarily pass this debug: true
so we can see the event being dispatched in the log.
... lines 1 - 3 | |
import { useDispatch } from 'stimulus-use'; | |
... line 5 | |
export default class extends Controller { | |
... lines 7 - 12 | |
connect() { | |
useDispatch(this, {debug: true}); | |
} | |
... lines 16 - 44 | |
} |
Now, down after success, say this.dispatch('success')
.
... lines 1 - 5 | |
export default class extends Controller { | |
... lines 7 - 24 | |
async submitForm(event) { | |
... lines 26 - 28 | |
try { | |
... lines 30 - 35 | |
this.dispatch('success'); | |
} catch (e) { | |
... line 38 | |
} | |
} | |
... lines 41 - 44 | |
} |
Try it: reload... and try selling some rotted, I mean, reclaimed wood at our store. Submit and... awesome! That did just dispatch an event... and its name is modal-form:success
.
Copy that. Now that we've seen the event name, go back over and remove the debug
option. Here's the last magic piece to make this work. In index.html.twig
, up on the reload-content
controller <div>
, add an action for the new event: data-action=""
the name of the event - modal-form:success
- arrow, the name of the controller - reload-content
- a pound sign and the name of the method: refreshContent
.
... lines 1 - 4 | |
{% block body %} | |
<div | |
... lines 7 - 10 | |
data-action="modal-form:success->reload-content#refreshContent" | |
> | |
... lines 13 - 37 | |
</div> | |
{% endblock %} |
That's it! When the modal-form:success
event is dispatched, it will bubble up to this element and we will call refreshContent()
. Then... that'll take care of the rest!
Let's test it. Reload the page, open the modal and let's sell some avocado peels. Submit. Ah! It... kinda seems like it worked? Except that the Ajax endpoint apparently returned the entire page, not just the template partial.
Let's go look at the controller. Ah... I warned about this and then did it anyways! We're using fetch()
to make the Ajax call... and fetch()
does not send the header needed for isXmlHttpRequest()
to work. And so, this always renders index.html.twig
.
That's okay! Let's just add a query parameter to the end of the URL. I like that better anyways.
Replace this code with $request->query->get('ajax')
. So, we'll be looking for a ?ajax=1
on the end of the URL.
... lines 1 - 15 | |
class ProductAdminController extends AbstractController | |
{ | |
... lines 18 - 20 | |
public function index(ProductRepository $productRepository, Request $request): Response | |
{ | |
$template = $request->query->get('ajax') ? '_list.html.twig' : 'index.html.twig'; | |
... lines 24 - 27 | |
} | |
... lines 29 - 104 | |
} |
In the template - index.html.twig
- add that to the URL by passing extra params with ajax
set to 1
.
... lines 1 - 4 | |
{% block body %} | |
<div | |
... line 7 | |
{{ stimulus_controller('reload-content', { | |
url: path('product_admin_index', { ajax: 1 }) | |
}) }} | |
... line 11 | |
> | |
... lines 13 - 37 | |
</div> | |
{% endblock %} |
Try the form again. Refresh! We'll sell some salsa to go with those avocados... and this time... perfect! The section reloaded. We're done!
If we want to make this a bit fancier, we could even add some classes and use those to force CSS transitions. Or we can do an even simpler trick. When it first starts loading, let's say this.contentTarget.style.opacity = .5
.
Copy that, and then, after it finishes loading, set the opacity back to 1.
... lines 1 - 2 | |
export default class extends Controller { | |
... lines 4 - 8 | |
async refreshContent(event) { | |
this.contentTarget.style.opacity = .5; | |
... line 11 | |
this.contentTarget.innerHTML = await response.text(); | |
this.contentTarget.style.opacity = 1; | |
} | |
} |
Add one more product: this time, a mystery box of donuts. Watch the table closely when I hit save. Yes! It was quick, but the table had less transparency for just a moment while it reloaded.
Now that we've got this cool new re-usable reload-content_controller
, let's use it to completely replace our custom cart-list
controller. Yay for less code! That's next.
// 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
}
}
Could it be that the target option in Stimulus-use is not working correctly? I have a big controller "test" which contains an element that I marked as target. That target also has a second controller "receiver". I want to dispatch an event from the big test controller to the receiver. If I do it with stimulus-use it doesn't work. But with plain JS it does.
`
target: this.eventDispatchTarget,
detail: {}