Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Symfony UX: Stimulus

4:55:39

What you'll be learning

This tutorial works perfectly with Stimulus 3!
// 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.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
    }
}

Symfony UX is all about helping you build better JavaScript interfaces faster.

The first piece of UX is Stimulus: a JavaScript library built around the idea that your server should return HTML. Stimulus gives you the ability to add JavaScript to any part of your page in an object-oriented way that you will love:

  • Installing Stimulus & the Stimulus Bridge
  • Stimulus controller basics & "instances"
  • Adding targets and using actions
  • Replacing a select element with a JS-powered color selector
  • Managing "state"
  • Stimulus values API
  • Free Stimulus libraries! stimulus-use and stimulus-components
  • The UX PHP packages: building chart.js in PHP!
  • Using React, Vue or some other frontend framework
  • Laziness: loading controllers lazily

By the end, you'll be able to build anything on your site with Stimulus. And once you have, you'll be ready to give your app a SPA (single page app) feel with Turbo!


Your Guides

Ryan Weaver

Buy Access
Login or register to track your progress!

Join the Conversation?

31
Login or Register to join the conversation
akincer Avatar
akincer Avatar akincer | posted 1 year ago

My mind is now fully blown by how easy and intuitive stimulus makes doing front-end javascript dev without abandoning Twig. Thanks and I can't wait for the Turbo course!

2 Reply
Krzysztof K. Avatar
Krzysztof K. Avatar Krzysztof K. | posted 3 months ago

Is there any part of tutorial on symfonycasts about pagination in stimulus? How to deal with pagination using KnpPaginator and Stimulus?

1 Reply

Hey Krzysztof,

Unfortunately, there's no a course about Stimulus and Symfony yet. But if you follow this course till the end - you should know Stimulus features and I believe could implement this. In theory, you just need to send an AJAX request to the server requesting a special page, and then render the records including a pagination buttons below of it back to the client where you just replace the current page content (including pagination buttons) with the new one - it should be that easy.

I hope this helps!

Cheers!

Reply

Hello, please, any recommendations of bundles for SEO, a hug

1 Reply

Hey yoelkj

Have you seen SonataSeoBundle? I haven't used it but it looks mature enough
https://github.com/sonata-p...

Cheers!

1 Reply

Hi,

That’s a really great course! Thank you.
I have just one question though: how to test stimulus controllers?
I guess you will answer like "exactly like any other basic JS code", but one or two example would make this course even better.

BR,
Florent

1 Reply

Hey Spomky!

This is a really good question - and I thought about including it in this tutorial... but it was already so long... so (right or wrong) I left it for later. I can say 2 things:

A) exactly like any other basic JS code 😉. For example, you could use Panther with Symfony's functional testing tool. That is, on its own, a whole other tutorial we need to do.

B) The Symfony UX libraries have some testing tools & examples and they leverage a cool library called "@testing-library/dom" which allows you to do JavaScript testing with a "mock" DOM. Here's an example: https://github.com/symfony/... - I think this is a SUPER cool way to test.

Let me know what you think!

Cheers!

1 Reply

Many thanks for your answer!

A) OK I will go with Panther. While waiting for your dedicated course, I will read some papers and video about it.
B) I found this package: @symfony/stimulus-testing (https://github.com/symfony/.... Looks perfect with a neat documentation.
Thank you for the advice, all you do really help A LOT. All the best to you.

Reply

Hey Spomky!

Nice job finding https://github.com/symfony/... - I know that Titouan (its author and original author of the UX stuff) is particularly excited about it :).

Cheers!

Reply
Nicolas-R Avatar
Nicolas-R Avatar Nicolas-R | posted 20 days ago

There is a plan to add a video about testing an stimulus controller?

Reply

Hey Nicolas,

We don't have specific plans to cover Stimulus controllers specifically. Well, we do have a few tutorials about testing, and in this case you can take a look at Behat testing that helps you to do a functional test including JS behavior, e.g. starting from this specific video: https://symfonycasts.com/screencast/behat/javascript-waiting - so you can test that your Stimulus JS code works properly for your clients this way. But if you're talking about unit testing Stimulus controllers - I suppose you just need to find any JS unit testing framework, but nothing available on SymfonyCasts about it yet.

Cheers!

Reply
Krzysztof K. Avatar
Krzysztof K. Avatar Krzysztof K. | posted 4 months ago

I have stimulus controlled list on page with 'new', 'edit' and 'remove' action in modal forms. Everything works great. List is reloading after all actions correctly.
I'm displaying second list below (without those actions) with same elements after full refresh.

And now I need to reload contents of second list with stimulus-reload on modal forms submit on first list. In short - I need to reload both lists. Second list is outside of top block of first list.

How to do this?

Reply

Hey Krzysztof K.

I think this is a good case for using events. You could dispatch an event when reloading the first list so the second list can listen it and act accordingly

Cheers!

Reply
Krzysztof K. Avatar

It works! Few changes in modal-form controller to dispatch correct thing and it works like a charm :) Thanks!

1 Reply
Markus L. Avatar
Markus L. Avatar Markus L. | posted 5 months ago

Heyho, dear Symfoneers!
At a very small (minimal) local project/page with a stimulus controller (the demo project home page from Ryan's marvelous talk on "Symfony UX" at "SymfonyWorld Online 2022 Summer Edition") I found a pretty large footprint of 1.1 sec (dev) or still 0.9 sec (prod) until DOMContentLoaded. Is this expected or rather an artefact of my local setup ? Can this be decreased ?
Best, Markus

Reply

Hi Markus L.!

Sorry for the very slow reply!!! And thanks for the nice words :).

> I found a pretty large footprint of 1.1 sec (dev) or still 0.9 sec (prod) until DOMContentLoaded

To make sure I'm checking the right spot, are you referring to this app? https://github.com/symfony/...

If so, then we can easily check it out on https://ux.symfony.com/ - when I look there, I also see a DOMContentLoaded of around 600-700ms. To be honest, that's not a metric that I've really ever thought much of before. But checking some of my favorite sites - e.g. Stackoverflow, GH, those all seem to be slower than that (which makes sense, those are heavier sites).

Anyways, the DOMContentLoaded in this case shouldn't have anything to do with Stimulus - I believe that would more be about how much HTML you have on the page and whether or not you having "blocking" assets, like scripts. Stimulus itself doesn't block anything, and the content (since it's just normal, boring HTML) is rendered by your browser, even before Stimulus fully initializes (i.e. we're not waiting on Stimulus to make the page render correctly).

Cheers!

Reply
Markus L. Avatar

I meant this one: https://github.com/weaverry... :) At first sight it did not look to me like it was the HTML, because there's hardly anything on the page.

Reply

Ahhh! Hmm, I'm not sure then - that's a *dead* simple (and small) HTML site, so I'd imagine that most of the time waiting would be... from the server? (I'm assuming that DOMContentLoaded time starts when you originally request the page... so a slower server response would increase this - but I'm guessing). Anyways, my guess is that this is not something to worry about - Stimulus and Symfony UX operate after the HTML is loaded... so if you're optimizing DOMContentLoaded, you're really trying to optimize, I believe, server response time and (possibly) the amount of HTML that needs to be parsed (and also not having blocking script tags, etc - but the standard setup in Symfony uses "async" to avoid blocking).

Let me know if this makes sense - or if some parts of the loading still look weird to you :).

Cheers!

Reply
Markus L. Avatar

Coolthnx :)

1 Reply

A little bit off-topic, but... i swear that i watched a part of the video: Ryan's marvelous talk on "Symfony UX" at "SymfonyWorld Online 2022 Summer Edition") but... i didn't finished it and i want to watch it.. where can i find the vid?

1 Reply
Kaizoku Avatar

Hi team, thank you for this great tutorial on Stimulus.
I'm definitely not a JS ninja so the great job you do is very helpful to me.

I am coding a web3 type app and I'm getting stuck on some points.

1 - What is the good practice to have a constant file shared between several stimulus controllers?

Ex: I have a const BEST_DINO = "T-REX" and I would like not to define it in each controller.
( in reality, I have a huge table of over 400 lines and it's a very ugly to copy / paste it into each controller).

2 - How do I share an object instance between my controllers?

Ex: Controller A is in charge of instantiating an object and to configure it (it is an external lib).

`this.provider = new ethers.providers.Web3Provider(***window***.ethereum)`;

then I do some check that is using the above variable 'this.provider'

this.login();
this.detectNetwork();
...

Now how can I use this same instance in my Controller B ? and I don't want to merge Controller A and B.

3 - How to listen on an event that is not attached to HTML via an action ?

Ex : I am using the Chrome Metamask plugin. during the page load I can detect if the user is connected and on the correct network.
However, I cannot detect the event when he manually changes networks.
The code that is supposed to work look like this


// Force page refreshes on network changes

const provider = new ethers.providers.Web3Provider(window.ethereum, "any");
provider.on("network", (newNetwork, oldNetwork) => {
// When a Provider makes its initial connection, it emits a "network"
// event with a null oldNetwork along with the newNetwork. So, if the
// oldNetwork exists, it represents a changing network
if (oldNetwork) {
window.location.reload();
}

Thanks !

Reply

Hey Kaizoku!

Nice to chat with you - and I'm super happy to hear that the tutorial was of some use to you! Now, to the questions:

> 1 - What is the good practice to have a constant file shared between several stimulus controllers?

The solution to this, nicely, has nothing to do with Stimulus. You can just create variables (variables, objects, functions, whatever) in one file, export those, and import them from your Stimulus controller. So, for your example:


// assets/data.js
export const BEST_DINO = 'Spinosaurus'

(I had to change to Spinosaurus, my son's current favorite dino ).
Then, you can use that inside Stimulus or anywhere else:


// assets/controllers/my-controller.js
import { BEST_DINO } from '../data';

You can also export a giant "object" from your file. Heck, if you have a .json file, you can even import that directly. Iirc, it looks like:

😉
import dinoData from '../data.json';

> 2 - How do I share an object instance between my controllers?

Hmm. There are probably a few ways to solve this. But once again, it's a JavaScript trick. To help manage this Web3Provider, let's create a re-usable JavaScript module (fancy word for "file that exports something"):


// "tools" is not important - just showing you can organize things however you want
// assets/tools/web3_provider.js

let provider;

export function getWeb3Provider() {
if (!provider) {
provider = new ethers.providers.Web3Provider(***window***.ethereum)`;
}

return provider;
}

Now you can, from anywhere:


import { getWeb3Provider } from '../tools/web3_provider';

getWeb3Provider().someMethod();

This will always return the same one instance. This works thanks to two tricks inside JavaScript:

A) If you import web3_provider.js 5 times, really JavaScript loads that file just ONCE. The next 4 times, it returns the same value from the first.
B) And so, putting that let provider; means that, if you import web3_provider.js 5 times, they are all, sort of, "sharing" that value. The first time we call getWeb3Provider() from one place, it will be set. The next time we call that function - even if it's from a totally different file - the provider variable will already be set.

> 3 - How to listen on an event that is not attached to HTML via an action ?

Stimulus actions are a great shortcut for making it easy to attach listeners to HTML elements. However, if you don't have that situation (in this case you're listening to an object), then actions won't help you. And that's fine! Just use your normal code. To make it nice in Stimulus, I might write it like this:


connect() {
const provider = getWeb3Provider();
// this is just a nice way to have a method called
// the .bind(this) is weird, but ensures that "this" will refer to this
// controller object inside that method
provider.on("network", this.onProviderChange.bind(this));
}

onProviderChange(newNetwork, oldNetwork) {
if (oldNetwork) {
window.location.reload();
}
}

I hope this helps!

Cheers!

Reply
Kaizoku Avatar

Hi Ryan,

Thank you so much for your answers, it really helps clear things up.
I think I had trouble integrating that you can simply combine Stimulus and normal JS.
I followed your advice and it's much better now.

to thank you, you can claim one (or more) free SPINOSAURUS NFT on this showcase website I developed : https://selenite-lab.xyz/er...
You will need a web3 wallet like metamask with the Polygon testnet configured, I explain this on the home page https://selenite-lab.xyz
Use a blank metamask wallet so it's 100% safe, as I said it on tesnet so this is not "real money" involved.

thx again :)

Reply

Haha, cool! Cheers!

Reply
Rich R. Avatar
Rich R. Avatar Rich R. | posted 1 year ago

Hi,

Great course, really recommend it.

I have a general question about Stimulus and state, based on a tree view controller I have created to show dynamic hierarchical data. The tree view controller works more or less great in about 50 lines of JS in a Stimulus controller: The only functions needed are to show/hide children and dispatch an event when a branch is selected. The "less" part being how to maintain state of which branches in the tree are open (ie their children are shown) when a page refresh occurs (due to, for example a change to the hierarchy in another part of the tree)?

I'm currently thinking that I have to keep the state of each branch (say, open/closed) somewhere at the backend, also. Ideally in the session, rather than the DB, to reflect its transient nature. It feels like I'm missing something though, and even wondering if Turbo would solve this problem for me?

Thanks for listening,
Rik

Reply

Hiya @Rik702!

Sorry for the slow reply! I'm really happy you enjoyed the course - I LOVE working with Stimulus :).

Ok, let's see. I think you have 2 options:

1) Exactly what you said: whenever a tree opens/closes, you make an AJAX call back to some endpoint to store this information somewhere (e.g. the session). Then, when you render the page, you would pass this information into your Stimulus controller as a "value".

2) OR, you could use local storage in the browser. Whenever a tree opens/closes, you store that in local storage. Then, in the connect() method (so, when the controller originally loads), you read local storage to see which things should be opened or closed.

> It feels like I'm missing something though, and even wondering if Turbo would solve this problem for me?

Turbo CAN solve a lot of problems. But... probably not in this case. Well, technically, if you converted the "hide/show" buttons into real anchor tags with real URLs (and if you go to those URLs, you would see the page rendered with the correct trees open), then you could use Turbo. Basically, it would "reload" (without a full page refresh) the entire tree structure on each click. Maybe a bit overkill for something that can be done fairly easily with Stimulus. But it is a fun thought experiment. And, in case you needed it, this approach would allow you to bookmark / copy a URL, send it to someone, and have them be able to load the page with the same children hidden/shown.

Cheers!

Reply
EinzigTech Avatar
EinzigTech Avatar EinzigTech | posted 1 year ago

Hi Ryan... Is everything good at your part? There has been quite e few days since an update which is very unlikely of you, so was just wondering if everything was fine!!!

Reply

Hey Stiff,

Thank you for worrying about Ryan! Everything is good, Ryan needed to take a rest for a while. We will continue releasing new videos soon. Thank you for your patience and understanding! I hope everything is good at your part as well!

Cheers!

Reply
EinzigTech Avatar
EinzigTech Avatar EinzigTech | victor | posted 1 year ago

Hey Victor,

Thanks for your reply. It is a relief to hear that you guys are healthy and doing fine. Actually this pandemic situation makes people worry more than usual.

It is nice to have breaks now and then to rejuvenate. I wish you guys all the best...

Reply

Hey EinzigTech!

This was a really nice message from you - it actually meant a lot to hear it. I had a loss in my family, so you were very astute to notice my absence. I'm doing ok, and we'll hopefully have our regular schedule back for next week :).

Cheers!

Reply
EinzigTech Avatar

Hi Ryan... I am sorry to hear your loss. My condolences... If you need, take a break... I don't think anyone would mind about it.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!