Extending a UX Controller

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

When we use the chartjs controller from the UX package, we build all the data for that library in PHP then pass it directly into chart.js. The Chart object we create is turned into JSON... rendered on this data-view attribute... which is ultimately read from the core controller and passed right into chart.js.

So, for the most part, if you need to customize your chart, you can do that by playing with the data in PHP. For example, click into the Chart.js library and go to "Get Started".

Configuring a Controller via data- Attributes

If you scroll down a bit, you can see that each chart has keys for type, data and options. In our controller, we're setting the data key via a nice setData() method. We can set the options in a similar way.

For example, there is apparently a scales, yAxes, ticks, beginAtZero option. Let's set that in PHP. Do that with $chart->setOptions()... and then we just need to match the options structure: scales set to an array, then yAxes, this is set to two arrays - you can see that in their JavaScript version - then ticks set to another array and beginAtZero set to true.

... lines 1 - 9
class AdminController extends AbstractController
{
... lines 12 - 14
public function dashboard(ChartBuilderInterface $chartBuilder)
{
... lines 17 - 28
$chart->setOptions([
'scales' => [
'yAxes' => [[
'ticks' => [
'beginAtZero' => true
]
]]
]
]);
... lines 38 - 41
}
}

Cool! Go back to our site. Our chart's y axis already starts at zero... so we won't see any difference. Oh, bah! A syntax error. I forgot my semicolon. Refresh now and... got it! It doesn't look any different, but if you look at the JSON on the data-view attribute, it now has our options data... which we know is eventually passed into chart.js.

So this is great! We can do many things right inside of PHP.

Adding a Second Controller to Extend the First

But... this can't handle every option. Go back to their docs, scroll down to "Developers" and click on "Updating Charts". You might have a situation where you need to update the data after it's rendered onto the page. This is easy in JavaScript: as long as you have that Chart object: change its data and call chart.update().

But how could we do that in our situation? In the core controller, it does create this Chart object... but we have no way to hook into the process and access that. Or do we?

In the connect() method of the core Stimulus controller, it does something very interesting: it dispatches an event! Actually two events: chartjs:pre-connect - where it passes the options on the event - and chartjs:connect - where it passes the Chart object itself!

How can we hook into these events? By creating a second controller that listens to them. Open assets/controllers/. Create a new file called, how about, admin-chartjs_controller.js. We'll start the same way as always. In fact, let's cheat: copy the inside of counter_controller.js and paste. Then add our normal connect() method with console.log() a chart.

import { Controller } from 'stimulus';
export default class extends Controller {
connect() {
console.log('📈');
}
}

Multiple Controllers on an Element

Next, in the template, and a second controller to the element. But, hmm. The render_chart() function is responsible for rendering this <canvas> element. Now we need to pass a second data-controller to this. How can we do that?

The answer is that render_chart() has an optional second argument: an array of additional attributes for the element. Pass data-controller set to the name of our controller, which is admin-chartjs. Oh, but I should probably write Twig code here... not PHP.

... lines 1 - 2
{% block body %}
... lines 4 - 101
<div class="col-10 mt-4">
<h1>Admin Dashboard</h1>
{{ render_chart(chart, {'data-controller': 'admin-chartjs'}) }}
</div>
... lines 107 - 108
{% endblock %}

Okay! Move over and hit refresh. The graph is still there and... yes! It looks like our new controller is connected!

Inspect the chart again. Interesting. You can't have two data-controller attributes on the same element. Fortunately, Stimulus does allow us to have 2 controllers on the same element by having one data-controller attribute with each controller separated by a space. The render_chart() function took care of doing that for us.

Hooking into the Core Controller via JavaScript

So here's the goal. Let's pretend that we need our new Stimulus controller to change some of the data on this chart and then re-render it. Maybe... we make an Ajax call every minute for fresh data.

This means we need the Chart object that's in the core controller. And that means we need to listen to the chartjs:connect event.

How do we do that? We already know that custom events are no different than normal events. And in this case, the event is being dispatched on this.element: the canvas element. We can add an action to that.

Over on render_chart(), I'll break this onto multiple lines. Add another attribute: data-action set to the name of the event - I'll go copy that from the core controller, chartjs:connect, an arrow, the name of our custom controller - admin-chartjs - a pound sign and then the name of the method to call when this event happens. How about onChartConnect?

... lines 1 - 101
<div class="col-10 mt-4">
<h1>Admin Dashboard</h1>
... line 104
{{ render_chart(chart, {
'data-controller': 'admin-chartjs',
'data-action': 'chartjs:connect->admin-chartjs#onChartConnect'
}) }}
</div>
... lines 110 - 113

Copy that and head into our custom controller. Rename connect() to onChartConnect(), give it an event object, and console.log(event).

... lines 1 - 2
export default class extends Controller {
onChartConnect(event) {
console.log(event);
}
}

Alright! Let's see if it works! Refresh, check the console and... we got it! There's the custom event! Expand it. I love this: it has a detail property, with the chart object inside.

Back in our controller, we now have access to the Chart object! And so we are infinitely dangerous. To test this out, let's see if we can let the chart load, wait 5 seconds, then update some data.

Start by assigning the chart to a property so we can use it anywhere: this.chart = event.detail.chart.

... lines 1 - 2
export default class extends Controller {
onChartConnect(event) {
this.chart = event.detail.chart;
... lines 6 - 9
}
... lines 11 - 15
}

Then, at the bottom, and a new method that will, sort of, fake making an Ajax request for the new data and updating the chart. I'll call it setNewData(). Inside, say this.chart.data.datasets[0].data[2] = 30 and then this.chart.update().

... lines 1 - 11
setNewData() {
this.chart.data.datasets[0].data[2] = 30;
this.chart.update();
}
... lines 16 - 17

This first line might look a little crazy... but if you look at their docs, this is how you can access your datasets. Let me go to the data we created in our PHP controller: we have a single "dataset". So we're finding the 0 index to get this dataset, which is this stuff, finding the data key, finding the element with index 2, and changing it to 30. So that should change the 5 up to 30.

Back in the Stimulus controller, up in onChartConnect() call setTimeout(), pass that an arrow function, wait 5 seconds and then call this.setNewData().

... lines 1 - 3
onChartConnect(event) {
... lines 5 - 6
setTimeout(() => {
this.setNewData();
}, 5000)
}
... lines 11 - 17

Moment of truth. Head over, go back to our site and reload the page. Here's the chart. Waiting... ha! It updated! March jumped up to 30!

This was all possible thanks to the fact that the chartjs core Stimulus controller dispatches these events. That gives us 100% control over its behavior.

And this isn't something unique to this one controller. This is a pattern that many of the UX libraries follow.

Next: if you've been wondering how things like React or Vue.js fit into Stimulus, wait no longer! The answer is that, while you might choose to use them less, if you do want to build something in React or Vue, they work beautifully with Stimulus.

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