Hotwired ASP.NET Core Web Application — Part 5

ipek
10 min readJun 18, 2022

--

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:

Installing Stimulus as a runtime dependency

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:

Initializing Stimulus

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:

hello_controller.js

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.

Each element is assigned its own Stimulus controller instance

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:

Button with a data-action attribute

And we will also add the greet method to our hello_controller.js:

hello_controller.js with greet method

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.

Action descriptor connected to the action method

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:

  1. this.hasUsernameTarget signifies whether a target with the name “Username” exists or not.
  2. this.usernameTarget property is assigned to the first matching target.
  3. 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”.

Using stimulus target properties

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":

div controller element with the counter value data attribute

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.

Defining values in the controller

And just like in targets, Stimulus will define the following properties for each value in the controller:

  1. this.counterValue can be used to read and change the value with the necessary type conversions.
  2. 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.

Accessing a value using the auto-generated properties

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:

Updating counter value after each greet

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:

  1. this.emptyClass: This will give us the first class value of the CSS class attribute.
  2. this.emptyClasses: This will give us the array of all classes of the CSS class attribute.
  3. 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:

Controller code handling the input event and changing the CSS class on the input element

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:

  1. Inside the connect callback of Stimulus, to initialize the style when the controller connects.
  2. 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

--

--