Hotwired ASP.NET Core Web Application (6 Part Series)
Hotwired ASP.NET Core Web Application — Part 1 (Intro)
Hotwired ASP.NET Core Web Application — Part 2 (NPM, Webpack Setup)
Hotwired ASP.NET Core Web Application — Part 3 (Turbo Drive & Frame)
Hotwired ASP.NET Core Web Application — Part 4 (Turbo Stream)
Hotwired ASP.NET Core Web Application — Part 5 (Stimulus)
Hotwired ASP.NET Core Web Application — Part 6 (Quote Editor)
In part 3 and part 4, we covered Turbo components and now it’s time to utilize Stimulus in our application. First, we will install Stimulus from npm
. Open a terminal window, go to the ClientApp
directory and run the following command:
Next, we will create the folder controllers
under the ClientApp\js
, and we will add the following lines to the ClientApp\js\app.js
file:
In the above code, we first import Application
from the Stimulus. Then we call webpack’s require.context
helper with the path to the folder that will contain our Stimulus controllers, and in our application it is the ClientApp\js\controllers
folder that we just created. Then, we pass the resulting context to the Application#load
method using the definitionsFromContext
helper of the stimulus-webpack-helpers
package. This setup enables the automatic loading and registration of all Stimulus controller
files.
Now before getting into what Stimulus is and how it works, let’s first see it in action and then explain the concepts using the example. We will go on using our Index.cshtml page as a playground to learn Stimulus, as we did for Turbo. We will start with adding some HTML under the Stimulus logo. Notice that we also added a custom attribute (data-controller="hello"
) to the div
element:
And create a file named hello_controller.js
inside the ClientApp\js\controllers
folder and replace its content with the following code:
Now reload the page and you will see the “Hello from the ‘hello’ controller.” log in the Console tab.
Controllers
Stimulus connects JavaScript objects to elements on the page and these objects are called controllers.
Stimulus continuously monitors the page waiting for HTML
data-controller
attributes to appear. For each attribute, Stimulus looks at the attribute’s value to find a corresponding controller class, creates a new instance of that class, and connects it to the element.
When we added the hello_controller
and then set its identifier (hello) to the data-controller
attribute of the div
element, Stimulus automatically created an instance of the hello_controller
class and connected it to the div
. And this is the main purpose of the Stimulus: automatically connecting DOM elements to JavaScript objects (controllers).
What if we had two elements on our page with their identifier set to hello in their data-controller
attribute? Then, each element will its own instance of the controller. We can test this by duplicating our previous HTML so that we will have two div
elements with the data_controller="hello"
. And when we reload the page, we will see two log statements on the console.
We define our controller classes as JavaScript modules in separate files. And their naming is important since it specifies their identifier. We should name them like [identifier]_controller.js
, where [identifier]
corresponds to each controller’s identifier. You can read more about the naming conventions in Stimulus from here and here.
As we see in the hello_controller.js
, we export each controller class as the module’s default object.
We called our sayHi
method from the special connect
method of Stimulus which is called each time a controller is connected to the document. These special methods are called lifecycle callbacks. There is also the disconnect
callback, which is called anytime the controller is disconnected from the DOM, and the initialize
callback, which is only called once when the controller is first instantiated.
Remember the const application = Application.start()
line, we have in app.js
file, which created a Stimulus Application
instance. Every controller belongs to a Stimulus Application
instance which we can access using this.application
inside our controller. And also, every controller is associated with an HTML element that can be accessed using this.element
property. We have outputted this.element
in the sayHi
method, and it is logged as the div
element that has the data-controller
attribute.
There is a lot to mention about controllers and the remaining concepts. And Stimulus handbook and reference documentation clearly explain every feature with small examples, so I won’t mention each of them. But I would again advise you to refer to each section after, we have to implement them in our code.
Now, on top of controllers, there are three other main concepts in Stimulus:
1. Actions
With actions, we connect controller methods to DOM events using data-action
attributes.
Let’s change our HTML like the following to add a data-action
attribute on the button:
And we will also add the greet
method to our hello_controller.js
:
As you might have guessed, we connected the button’s click
event to the newly added greet
method. You should be able to see the console log message when you click the Greet button.
Here the value of the data-action
attribute, which is click->hello#greet
, is called the action descriptor which consists of the event name->controller identifier#action method
name parts. And the greet
method handling the click
event is called the action method.
What if we want to access the button that is clicked inside our controller? An action method’s first parameter is the DOM event object. If we define it, we can capture and utilize it. In the following code, it is captured in the eventObj
variable and we log its target
property to get the element that dispatched the event which is our button
element.
2. Targets
With targets, we can reference important elements by name in the controllers. We can define a target with the following target attribute
format:data-{controller name}-target
Let’s define a target attribute
on our input
element with the value username
:
We can then define target names in the corresponding controller class, using static targets
array:
When we define the targets as above, Stimulus automatically creates the following three properties that we can use in our controller:
- this.hasUsernameTarget signifies whether a target with the name “Username” exists or not.
- this.usernameTarget property is assigned to the first matching target.
- this.usernameTargets property is an array of all matching targets.
We should note that if we use the this.usernameTarget property when there is no matching element, it will throw an error. So, we should use the this.hasUsernameTarget property before accessing that property to be on the safe side or if the target existence is optional. In the below code, we added a new console log statement inside the greet
action method. It first checks the existence of the username target and then logs its value if it’s not empty, otherwise logs “no one”.
3. Values
We can read and write to data-*
global attributes on controller elements as typed values using the following data attribute format:
data-{controller identifier}-{value name}-value
Let’s set a data attribute for the value counter
of type Number
on our controller element div
, with data-hello-counter-value="0"
:
We should also be aware that values can only be created on the controller elements which are the elements that have the data-controller attribute. In the previous example, the div
element was the controller element with the data-controller="hello"
attribute. But if we had set the data-hello-counter-value="0"
attribute on the input
or button
elements inside the div
, it wouldn’t work.
And in the controller, the corresponding values are defined using static values
object by placing the value’s name
on the left and the value’s type
on the right.
And just like in targets, Stimulus will define the following properties for each value in the controller:
- this.counterValue can be used to read and change the value with the necessary type conversions.
- this.hasCounterValue is used to check the existence of the value.
But unlike a target, using the this.counterValue property when the counter
value doesn’t exist in the HTML, doesn’t throw an error; instead, it returns the type’s default value.
Here, we added the last line inside our greet
action method that increments the value of the counter
value by 1.
We can also track the changes on the value attributes by declaring a method with the [value name]ValueChanged
name format. And this is what we will use to output the current greeting count:
And here is the console log output when we clicked the Greet button three times:
We see that our counter
value is correctly incremented after each greet. But there is one more thing to notice: we see that the data-hello-counter-value
attribute value also changes and keeps the last counter
value. And this is where values
are used: to keep the state in the DOM elements. While other frameworks keep the state in JavaScript files, Stimulus JavaScript files (controllers) are stateless.
Until now, we have covered the four main concepts in Stimulus: controllers, actions, targets, and values. There is one more subject where Stimulus makes our lives easier: manipulating CSS classes.
CSS Classes
As we know, CSS classes are a set of styles that are applied to HTML elements. And generally, we need to change the style of an element based on a condition, such as a certain user action or a value change, in JavaScript files. However, while doing that we are hard-coding class names in string. Stimulus provides an alternative solution to this problem by enabling us to refer to CSS classes by logical names again by combining data attributes and controller properties.
Let’s make our input element have a different style when it’s empty. First, we will define a class with a logical name empty
in the static classes
array, as follows.
And similar to targets and values, Stimulus will automatically generate these three properties:
- this.emptyClass: This will give us the first class value of the CSS class attribute.
- this.emptyClasses: This will give us the array of all classes of the CSS class attribute.
- this.hasEmptyClass: This will indicate whether the CSS class attribute exists or not.
Before using these properties, let’s add our CSS class attribute on our controller element.
In the above code, we added a data-hello-empty-class CSS class attribute on our controller element div
. And as you noticed, although we are going to change the style of our input
element, we do not define the CSS class attribute on that. Like values, CSS class attributes must be defined on the controller element.
Stimulus automatically maps the logical names in the static classes
array to the CSS class attributes on the controller’s element. CSS class attributes should follow this naming convention:
data-{controller identifier}-{logical-name}-class
In the above code, we also added a data-action
attribute on our input
element, to handle its input
event, so that we can decide whether to apply the empty
class or not when its content changes. Now, when we refresh our page, we will not see any change, since we didn’t write the necessary controller code to listen to the event and apply\remove the empty
class on our input
element. Let’s add some code to do that:
Using the above code, which mostly contains the changes, we defined a styleUsername
method that checks the content of the username
input and adds the empty
class, if its content is empty or removes it if it’s not. And we call this method in two places:
- Inside the
connect
callback of Stimulus, to initialize the style when the controller connects. - Inside the
usernameChange
action method which is called when the input changes.
Now when you refresh the page you will see the username input background yellow. But when you enter a value, the background will return to white. If you again clear the value, its background will again be yellow.
We applied only one CSS class for our empty
logical name. What if we wanted to change both the background and focus ring of the username
input with our empty class by changing it like the following?
When we change the CSS class attribute value as in the above and refresh the page, we will not see the green ring around our input: only yellow background will be applied to it as before.
To apply more than one CSS class with our empty
logical name, we should use the this.emptyClasses property instead of the this.emptyClass. Remember that the singular property would only return the first class on the list, while the plural one would return all of the classes in an array.
If we change our styleUsername
method as above, using the this.emptyClasses property with the spread syntax (…), we will be able to apply or remove multiple classes at once.
Summary
In this part, we have covered all the concepts in Stimulus. And unlike other JavaScript frameworks, we did not use Stimulus to create our HTML; we only used it to manipulate our DOM elements inside our server-side generated HTML. Stimulus allowed us to connect our DOM elements to JavaScript objects using controllers. And then, always by using a combination of data attributes and combination of data attributes and controller properties, it enabled us:
- to map our important DOM elements to targets
- to handle DOM events using actions
- to keep the state using values
- and to refer CSS classes using logical names
And lastly, if your code doesn’t work, refer back to the naming conventions of each concept. As we have seen, Stimulus does all its mappings through certain naming conventions!
Stimulus is really tiny but powerful. And there are quite a few resources out there, providing best practices and small components which I have listed in the references section. You can check them out to learn more about Stimulus or use these components instead of writing your own.
In the next and final part, I will briefly overview my Quote Editor Tutorial port to ASP.NET using everything I have covered. You can download the code covering this part from here.
References
[1] Stimulus: A modest JavaScript framework for the HTML you already have
[2] Stimulus Webpack Helpers
[3] Better StimulusJS
[4] Stimulus Components
[5] TailwindCSS Stimulus Components