Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

stimulus-bridge: How UX Packages Work

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

In the last video we installed a PHP package - symfony/ux-chartjs and it's recipe added a new JavaScript package to our package.json file, which points to a directory in that bundle. By running yarn install --force, yarn copied that into node_modules/ so that the package works like any normal JavaScript package.

So that was cool because, with basically one command, we got both a new bundle and a new JavaScript package, which contains a Stimulus controller.

Then, just by writing a little PHP code and calling a Twig function, boom! We magically had a JavaScript-powered chart. How the heck did that all work?

render_chart() just renders a data-controller Element

Inspect element on the graph. Interesting. All the render_chart Twig function actually did was render a canvas element with two important attributes: data-controller set to some long symfony--ux-chartjs--chart name, and a data-view attribute set to a JSON-version of the data that we built inside of PHP. In our controller, you can see that this matches the data we built here.

Look back at the ux-chartjs package in node_modules. In src/, open up controller.js. This is the controller that's being used to render that chart on our page. And it's beautifully simple. It reads that data-view attribute then instantiates a new Chart object. This comes from the chart.js package. That, ultimately, renders the chart into our element. So simple!

How Stimulus Controllers are Registered

But there is still one missing piece to explain how this all works. Open assets/app.js. The built version of this file is included on every page on our site. It loads bootstrap.js, which we opened up at the very beginning of the tutorial. The code inside this file looks a little weird, but its job is simple and important. It reads all of the files in our controllers/ directory and registers them with Stimulus as controllers. This is where the naming convention comes into play. When Stimulus sees a file called counter_controller, it registers that controller under the name counter. Then when a data-controller="counter" element appears on the page, it knows which controller to use.

import { startStimulusApp } from '@symfony/stimulus-bridge';
// Registers Stimulus controllers from controllers.json and in the controllers/ directory
export const app = startStimulusApp(require.context(
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
true,
/\.(j|t)sx?$/
));

If we ever added a data-controller to our page with a controller name that Stimulus does not know about like - data-controller="eat-pizza" - Stimulus will do nothing.

The point is: 100% of the controllers that Stimulus is aware of come from this line here, which means they come from the files in our assets/controllers directory.

controllers.json: Automatically Registered Controllers

But... wait. If you look back at our browser, this rendered a data-controller attribute set to symfony--ux-chartjs--chart. And... we do not have a file called symfony--ux-chartjs--chart_controller.js in this directory. So how the heck did the controller from our ux-chartjs package get registered as a Stimulus controller with this name?

The answer to that question lives in a file that we haven't really looked at yet: assets/controllers.json. This file was also automatically updated by the recipe when we composer required symfony/ux-chartjs. It was basically empty before.

{
"controllers": {
"@symfony/ux-chartjs": {
"chart": {
"enabled": true,
"fetch": "eager"
}
}
},
"entrypoints": []
}

When we first installed Webpack Encore, our package.json file came pre-filled with a few libraries. One of them is called @symfony/stimulus-bridge. If you look back at bootstrap.js, we import a function called a startStimulusApp from that package.

In reality, when we use that down here, that function does two things. First, it does what we already know: it finds all the files in our controllers/, directory and registers each as a Stimulus controller. Second, it reads our controllers.json file and also registers any controllers here as Stimulus controllers.

Let me show you how that works. When startStimulusApp() parses this file and sees the @symfony/ux-chartjs key, it finds that package in node_modules/ and opens its package.json. Then it looks for a special key called symfony and then controllers.

It then looks at the chart key... and uses that to find out exactly where this controller file actually lives: the path under this main key. We'll talk about the "eager" stuff later.

And that's it! For the controller name, it takes the package name - @symfony/ux-chartjs - then this controller nick name - chart and normalizes it into the long string that we see in the browser.

tl;dr: UX Packages give you Stimulus Controllers

If you don't care about too much about the details of how this all works, here are the cliff notes. Each time we install a Symfony UX package, we instantly - without doing anything other than composer require and yarn install - have access to a new Stimulus controller in our application. That's incredibly powerful.

For example, this doesn't exist yet, but you could imagine being able to install a form entity type auto-complete package in your app, which would give you a new Symfony form type that could replace the boring select element with an auto-completable field for selecting an entity. The possibilities are huge.

Next, let's investigate how we could control the behavior of the third-party Stimulus chart controller. It uses a really interesting pattern that will allow us to make big changes if we need to.

Leave a comment!

8
Login or Register to join the conversation
Krzysztof K. Avatar
Krzysztof K. Avatar Krzysztof K. | posted 1 month ago

Chartjs needs date adapter to bring Time Axis functionality. I've chosen to use date-fns adapter. But...

How to import it? Do I need some changes in controllers.json?

Reply
Krzysztof K. Avatar

Solution:


{
"controllers": {
"@symfony/ux-chartjs": {
"chart": {
"enabled": true,
"fetch": "eager",
"autoimport": {
"chartjs-adapter-date-fns": true
}
}
}
},
"entrypoints": []
}

Reply

Hey Krzysztof Kujawski

Great! Thanks for sharing your solution with others

Cheers!

Reply
Tac-Tacelosky Avatar
Tac-Tacelosky Avatar Tac-Tacelosky | posted 4 months ago

Where is the recipe that does the magic of adding the controller to bootstrap.js and the linked file to package.json? I'm trying to create a ux component (symfony bundle with stimulus controllers) and am not sure how the installation with happen. I've searched https://github.com/symfony/... for chart, ux, webpack, etc. trying to find where the magic happens.

Is there an example of how to create a third-party component, like ux-chartjs? Especially now that Symfony 6.1 makes it so easy to configure a bundle, I'm hoping to be able to contribute something.

Reply

Hey Tac Tacelosky

That's a great question. As far as I know it's an automatic process which is hardcoded in flex package and it uses composer.json "keywords" section. So if you have correct file structure and put in you keywords: 'symfony-ux' string, than composer will execute some fleex magic and add everything you need.

Cheers!

Reply
Tac-Tacelosky Avatar
Tac-Tacelosky Avatar Tac-Tacelosky | sadikoff | posted 3 months ago

Tried that, still no luck. Here's my trivial bundle: https://github.com/tacman/T..., and here's my open issue symfony/flex: https://github.com/symfony/...

Reply
SamuelVicent Avatar
SamuelVicent Avatar SamuelVicent | posted 1 year ago

The work done with Stimulus seems impressive to me, however there are some problems that seem to have not been taken into account when integrating with Symfony.

By having a single bootstrap.js, large amounts of Javascript are included in pages where it should not be. It would be nice to be able to create multiple bootstrap.js per context to include only those controllers that you want, personally I have been trying but I have not found the way to do it.

Reply

Hey Pablo Carballeda

In the following chapters Ryan shows how to controllers and modules dynamically. Perhaps that's what you need

Cheers!

Reply
Cat in space

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

This tutorial works perfectly with Stimulus 3!

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

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