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 SubscribeTo make our widget fully work, when we click a color, we need to change the real select element's value. That way, when we submit, the form will POST the correct color. Inspect element near the select
and look at its options: each value is the id of that color in the database. So somehow, when we click a color square, we need to know the id of that color so we can set it as the select element's value.
Fortunately, there's a native way to add extra information to DOM elements: data attributes!
Over in the template, find the button and add a new data attribute: data-color-id
equals {{ color.id }}
.
... lines 1 - 7 | |
{% for color in addToCartForm.vars.data.product.colors %} | |
<button | |
... lines 10 - 13 | |
data-color-id="{{ color.id }}" | |
... line 15 | |
></button> | |
{% endfor %} | |
... lines 18 - 37 |
This is the first time that we've used a data attribute that has nothing to do with Stimulus. We're just inventing this for our own purposes. The only rules about data attribute names is that they must, of course, start with data-
and they must be all lowercase. You also usually see dashes between words like color-id
.
Over in our controller, we could read the attribute manually... but we don't have to! JavaScript has a built-in way to read data-
attributes: it's the dataset
property.
At the bottom of selectColor()
, console.log(event.currentTarget)
- to get the button - then .dataset.colorId
.
... lines 1 - 2 | |
export default class extends Controller { | |
... lines 4 - 5 | |
selectColor(event) { | |
... lines 7 - 11 | |
console.log(event.currentTarget.dataset.colorId); | |
} | |
} |
Notice that the color-id
from the HTML becomes colorId
inside this dataset
property. This is... once again, not a Stimulus thing. This is just how data attributes work.
Let's test it out. Open the console and... when I click, yes! We see ids!
Now that we've got that, the next step is to find the select
element. And, whenever we need to find something, it means that we need a target.
Over in the controller, add a second target called, how about, select
.
... lines 1 - 2 | |
export default class extends Controller { | |
static targets = ['colorSquare', 'select'] | |
... lines 5 - 13 | |
} |
Then, in the HTML, add that target. Oh... but this is trickier because the form_widget()
function is rendering the select
element for us. No problem: we can pass a custom attribute. Add a second argument to form widget, pass an associative array, give this an attr
key set to another associative array with data-color-square-target
set to select
.
... lines 1 - 3 | |
{% if addToCartForm.color is defined %} | |
<div data-controller="color-square"> | |
{{ form_widget(addToCartForm.color, { | |
attr: { 'data-color-square-target': 'select' } | |
}) }} | |
... lines 9 - 19 | |
</div> | |
{% endif %} | |
... lines 22 - 39 |
Back over in the controller, assuming I haven't messed anything up, we should now be able to reference the select with this.selectTarget
. Set its value with .value =
and then event.currentTarget.dataset.colorId
.
... lines 1 - 2 | |
export default class extends Controller { | |
... lines 4 - 5 | |
selectColor(event) { | |
... lines 7 - 11 | |
this.selectTarget.value = event.currentTarget.dataset.colorId; | |
} | |
} |
Ok! Let's try this thing! Refresh click and... awesome! As we click the colors, the select
updates. This is fun!
At this point... we're done! This will work! To celebrate, let's hide the select
element. We could do that in Twig or in Stimulus - it's up to you. If you wanted to make your site work with and without JavaScript for some reason, you could hide the select
element and show the color boxes at the same time in Stimulus.
Anyways, in the controller, add a connect()
method. Then hide it with this.selectTarget.classList.add('d-none')
, which will add a display: none
since we're using Bootstrap.
... lines 1 - 5 | |
connect() { | |
this.selectTarget.classList.add('d-none'); | |
} | |
... lines 9 - 19 |
Go refresh. Oh, that is lovely. Let's add a red sofa to the cart. When we submit... I think it worked! Go check out the shopping cart. It did!
Now that this is working, go check out our Stimulus controller. Yup, this whole feature required about 15 lines of JavaScript! That's thanks to the fact that all of our markup gets to live in Twig. Then, our JavaScript can stay lean and mean.
Over in the browser, click to go back to the sofa page. Next: I want to allow the user to click a color again to unselect that color. That won't be too hard, but to make it even easier, let's take advantage of the fact that we can store state on our controller.
Hey Macarena,
Yes, it's that simple! Just replace the dash and uppercase the first letter after it. You just need to get used to it ;)
Cheers!
// 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
}
}
Hi!
I got a bit confused with the concept of color-id and colorId. This means that if I write color-otherid I invoke it as colorOtherid? Just that?