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 files 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!

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