Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Targets: Finding Elements

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Each time stimulus sees a new data-controller="counter" on the page, it instantiates a new instance of our controller class and calls this connect() method. From here, we can do anything! We can attach event listeners, change HTML or whatever else we dream up. And we can do it with or without jQuery: your choice. Heck we can even render React or Vue components from in here! That's something we'll do later.

But there are a few things that we do so often in a controller that Stimulus gives us some extra, optional, tools. The first helps us find elements inside of the controller element.

Here's the new challenge: instead of replacing all of the HTML when we click, I want to just change the number part.

Moving the Starting HTML into the Template

Head into our template - templates/product/index.html.twig - and remove the extra controller element: we don't need 2 anymore, that was just to test things out.

Inside the remaining element, let's add some HTML inside to start, like "I have been clicked" then <span> with class="counter-count", the zero inside, and then "times".

... lines 1 - 2
{% block body %}
... lines 4 - 9
<div class="col-xs-12 col-9">
<div data-controller="counter">
I have been clicked
<span class="counter-count">0</span>
times!
</div>
... lines 17 - 86
</div>
... lines 88 - 89
{% endblock %}

You might already know what I'm doing: I'm adding an element around the exact thing we want to change with a class so that we can use that class to find the element.

Oh, and by the way, this is the way we will normally work with Stimulus: we render HTML in the template - like we've been doing for years. Then we add behavior to some of that HTML in the controller. What I mean is, the controller isn't usually responsible for rendering the full content into the element, like you would do with React or Vue. The HTML belongs in the template.

Over in the controller, this means that we don't need the innerHTML logic anymore: that's already in the element.

Finding an Element without Targets

To replace just the number when we click, we need to find the span we created. Let's see - copy the counter-count class. To find this without jQuery, we can say const counterNumberElement = this.element - we're always going to start by looking inside the controller's element - then .getElementsByClassName() and pass this counter-count. This will return an array of all the elements, so we need to get the [0] index.

... lines 1 - 2
export default class extends Controller {
connect() {
... lines 5 - 6
const counterNumberElement = this.element
.getElementsByClassName('counter-count')[0];
this.element.addEventListener('click', () => {
this.count++;
counterNumberElement.innerText = this.count;
});
}
}

So... kind of easy... but not that easy to find the element. But it is enough to get the job done. Inside the click callback, replace this.element.innerHTML with counterNumberElement.innerHTML = this.count.

Sweet! Let's try it. Spin over, refresh and click. Beautiful!

Adding a "target"

But... finding that element was kind of ugly... especially because we're going to do that kind of stuff so often. Thankfully, Stimulus has a feature to make it so much nicer. It's called targets.

Here's how targets works. First, at the top of the class, add a static targets property. You can do that with static targets = an array and add count inside. count is the name of the new target we're creating and it could be anything: I just made that up.

... lines 1 - 2
export default class extends Controller {
static targets = ['count'];
... lines 5 - 13
}

Now, in the template. replace the class="" with a special data attribute: data-counter-target="count".

... lines 1 - 2
{% block body %}
... lines 4 - 11
<div data-controller="counter">
... line 13
<span data-counter-target="count">0</span>
... line 15
</div>
... lines 17 - 89
{% endblock %}

So that is very specifically named. It's data-, the name of our controller, dash the word target, equals, and then the name of the target. In this case, that's count.

Thanks to this, back inside the controller, we don't need to find the element anymore: it's already available! Remove the getElementsByClassName() and down inside the click callback, all we need is: this.countTarget.innerHTML = this.count.

... lines 1 - 2
export default class extends Controller {
... lines 4 - 5
connect() {
... lines 7 - 8
this.element.addEventListener('click', () => {
... line 10
this.countTarget.innerText = this.count;
});
}
}

That's it. Before we chat, let's go test it! Refresh... and... it still works!

Target Naming and Properties

So as soon as we have a target inside of our controller - in this case named count - we magically get access to a property called countTarget. That's one of the trickiest things about Stimulus: getting used to these naming conventions: like countTarget and data-controllerName-target="targetName".

Anyways, this.countTarget will either be an element or throw an error if no matching target was found. If there are multiple, it will return the first.

You can also use this.countTargets: that returns an array of all of the matching targets. We'll use that later to find each "color square" in a color selector that we're going to make.

Finally, if you want to check to see if a target exists, you can use this.hasCountTarget.

So... you will never again need to search for an element inside a controller. Targets have you covered. That's awesome!

The Class Properties Syntax

Oh, and I do want to say one thing about this targets = [] part. This syntax - where we set properties with an equal sign - is actually an experimental feature in JavaScript. To use it, in your webpack.config.js file, you need to have this configureBabel() line here: this adds a special plugin to Babel that gives us support for the syntax. The line comes automatically with the Encore recipe.

Next: there's one other super common thing that we always need to do in a controller: listen to events. Or, I guess you can think of this as "responding to user actions" like "click". Stimulus gives us a great way to handle this.

Leave a comment!

11
Login or Register to join the conversation
Lucinda M. Avatar
Lucinda M. Avatar Lucinda M. | posted 1 year ago | edited

Hi, I just wanted to add a bit of info about the static targets = [] syntax, since I've discovered it is not supported in Safari. The workaround is to use the alternative syntax:


static get targets() {
    return [];
1 Reply

Hi Lucinda!

When you run yarn run dev or yarn run watch, Babel is making sure in the background that you end up with code that current browsers can understand (this comes out of the box with encore, by the way!).

So, in reality, the code we end up with is:


Controller.targets = [];

Which is basically ES5 compatible and totally fine!

Reply

Hey Lucinda,

Oh, we didn't know it does not work in Safari somehow. Thanks for reporting it and sharing your solution with others!

Cheers!

Reply
Samuel W. Avatar
Samuel W. Avatar Samuel W. | posted 2 years ago

Hey Ryan,

great series, like always :D I see you are using PHPStorm as well. Do you know if there is a way to get rid of those "Unresolved variable"-squigglies under the values and target variables?

1 Reply
Kaizoku Avatar

Hi Samuel, 1 year later did you find something ?

Reply

Hey Samuel,

Unfortunately, I can't find any PhpStorm plugin for Stimulus, so the only way probably to disable unresolved vars check in the whole project, including JS files. I believe you can do it via PhpStorm Preferences -> search for "Unresolved variable" and uncheck the checkbox on "Unresolved JS var", thought I didn't try this. Here's the screenshot: https://imgur.com/a/NvL6qWz

Or just ignore them and maybe it will be fixed in future versions.

Cheers!

Reply
Tomasz N. Avatar
Tomasz N. Avatar Tomasz N. | Victor | posted 1 year ago

Hi Victor, maybe SymfonyCast should make that plugin even payble. I would gladly buy it for a dozen / several dozen dollars such a plug and I suspect that many others will also.
Regards.

1 Reply

Hey Hesus,

Thank you for your interest in this :) Unfortunately, we don't create plug-ins for PhpStorm, sorry, we only create tutorials. So it's up to community. If you know how to create PhpStorm plugin - please, feel free to create something that may help.

Cheers!

Reply
Shiraats Avatar
Shiraats Avatar Shiraats | posted 2 years ago

Hey

I'm really enjoying this Stimulus series but I have question around sharing data between Controllers. I would this be done? Also how would you update a Target that exist in One Controller with Action that exist in another Controller?

Reply

Hey @Shiraats!

Excellent question! To answer it fully, could you think of a real (or kinda real) use case for each part of your question?

but I have question around sharing data between Controllers. How would this be done?

There are ways to make controllers talk to each directly, but I kind of think this is a "last resort". The answer depends on the situation, but there are a few ways. For example, one controller could dispatch an event on a specific element - e.g. this.someElementTarget.dispatchEvent(new Event('some-event-name')). Then, another controller could listen to this event (with an action) on that element - e.g. &lt;span data-action="some-event-name->other-controller-name#methodName"&gt;.

I guess this sort of answers both your questions. Another way of sharing data is to set data onto the `dataset</code of elements that both controllers have access to. But again, it depends on your situation. I have not hit many situations yet where I have 2 controllers that need to talk to each other.

Cheers!

Reply
Shiraats Avatar
Shiraats Avatar Shiraats | weaverryan | posted 2 years ago | edited

Hi weaverryan

Thanks that makes sense. So its something similar to emitting an event in one vuejs component and then listening for that event in another component.

If I had to give an example of a use case in this tutorial it would be like adding an item to my cart which is managed by one controller and then updating the the number of items in the cart in the navbar which is managed by another controller.

1 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": ">=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
    }
}

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