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 SubscribeWhen we click a square, we need to add a border around the square to show that it's currently selected. I've already created a CSS class for this... I'll hack it into the page so you can see it. It's called selected
. It even comes with a nice little CSS transition! Ooooo.
Over in the controller, in the selectColor()
method, how can we figure out which of the three color squares was just clicked? The answer is always: event.currentTarget
.
Try this: event.currentTarget.classList.add('selected')
.
... lines 1 - 2 | |
export default class extends Controller { | |
selectColor(event) { | |
event.currentTarget.classList.add('selected'); | |
} | |
} |
Before we chat about this, let's make sure it works. Refresh, click and... beautiful! We can currently select multiple colors... which isn't ideal, but we'll fix that soon.
There are two important things about this line. First, when you listen to an event - or "action" in stimulus - the event object always has two similar properties: event.target
and event.currentTarget
. Sometimes these are the same element... and sometimes they're not.
Let me show you an example with some dummy code in the template. Imagine you have a button with an action on it - I'll reuse our existing data-action
. Inside the button we have some text... but some of that text is inside another element. A more realistic example might be that you have an image or FontAwesome icon inside.
In the controller, I'll temporarily comment-out our code and instead console.log()
event.target
and also event.currentTarget
so we can see the difference.
Go refresh the page. There's our stunning button! Open up the console.
First, click the text that's directly in the button. Nice! both event.target
and event.currentTarget
are the same thing: the button
element.
Now click the span that's inside the button. Woh! This time they're different! The target
is the span
while currentTarget
is still the button!
This is not a Stimulus thing: this is just how DOM events work. event.target
will always be the actual element that received the action. The second time we clicked, we were actually clicking the span
.
But event.currentTarget
will always be the element that we added the listener or action to.
So that's a long way of saying that event.currentTarget
is your friend, because it will return the element that we've attached our action to. So we always know what it's going to be. event.target
could be that element... or it could be a child element.
Let me remove that weird extra button... and then put the code back in our controller.
The other interesting thing on the line in our controller is classList
. This is a property on the native Element object and... as you can see, it's just an easy way to add or remove class. No jQuery or other fancy tools needed.
So... our color selector works great so far. Except for the problem that we can select multiple colors. We need to make sure that only one color has the selected
class at a time.
Let's think about how to solve this. One option would be to look for an element with the selected
class inside this.element
. And if we find one, remove the class.
Another option is to use a target. We could make each color square a target, then, on click, loop over all of them and remove the selected
class before re-adding it to the one that was just clicked.
Let's do that. First, define the target with targets = []
. Let's call the new target, how about, colorSquare
. I did just make a mistake: see if you can spot it.
... lines 1 - 2 | |
export default class extends Controller { | |
targets = ['colorSquare'] | |
... lines 6 - 10 | |
} |
Oh, and notice the naming of the target: it's lower camel case. I'm not using color-square
because the name of the target becomes a property.
Down in the method, let's console.log(this.colorSquareTargets)
.
... lines 1 - 5 | |
selectColor(event) { | |
console.log(this.colorSquareTargets); | |
... lines 8 - 9 | |
} | |
... lines 11 - 12 |
I put an "s" on the end on purpose: this will return an array of all matching targets.
Finally, in the template, let's add the target to the button
. Remember: the syntax for that is data-
the name of the controller - so color-square
- the word target
equals, then the name of the target: colorSquare
.
... lines 1 - 3 | |
{% if addToCartForm.color is defined %} | |
... lines 5 - 7 | |
{% for color in addToCartForm.vars.data.product.colors %} | |
<button | |
... lines 10 - 12 | |
data-color-square-target="colorSquare" | |
... line 14 | |
></button> | |
{% endfor %} | |
... line 17 | |
{% endif %} | |
... lines 19 - 36 |
Yes, you do need to write a few targets before you remember this syntax by heart. But you'll get it.
Let's try this. Refresh, click and... oh! Undefined?
Hopefully... you saw my mistake. Back in the controller, make this static targets. I made that mistake because... in the real world... I've made that mistake more than a few times before. This must be static and... if you forget, there's no huge error: it just won't add the magic target properties.
... lines 1 - 2 | |
export default class extends Controller { | |
static targets = ['colorSquare'] | |
... lines 5 - 10 | |
} |
Try it now. Refresh, click and... yes! We see the 3 button
elements.
Let's loop over these inside of our method: this.colorSquareTargets.forEach()
and the function will receive an element
. Inside, remove the selected
class from all of them for simplicity: element.classList.remove('selected')
.
... lines 1 - 5 | |
selectColor(event) { | |
this.colorSquareTargets.forEach((element) => { | |
element.classList.remove('selected'); | |
}); | |
... lines 10 - 11 | |
} | |
... lines 13 - 14 |
Let's try this one last time. Now when we click... yes! It works!
Next: let's put the finishing touches on our color selector widget by finding and updating the select
element's value whenever the user clicks a square. Then we'll finally hide the select element and let our color squares take center stage.
Hey Jakub G.
Yea... it may read better but the map
function is meant to create another array after applying your function to every of the initial array elements, and in this case we're not creating a new array
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
}
}
this one liner seems to be more elegant ( ͡° ͜ʖ ͡°)
this.colorSquareTargets.map(t => t.classList.remove('selected'));