Skills Turbo
We want to demonstrate the practical usage of Hotwire in a Ruby on Rails application, with PuzzleSkills as example. PuzzleSkills allows users to track their so-called skills, which include their qualifications, certifications, and their experience and interest in a particular technology.
The PuzzleSkills tech stack
The filter form already makes use of a wide range of technologies.
Hotwire
Hotwire alone is not really a singular technology, but rather «an alternative approach to building modern web applications without using much JavaScript by sending HTML instead of JSON over the wire.» To enable «HTML Over The Wire», the following technologies are essential:
Stimulus
Stimulus is a JavaScript framework that allows us to bring JavaScript functionality into static or server-side rendered webpages. This is done by connecting JavaScript objects to page elements.
Aditionally, to get started, create a Stimulus controller by executing ./bin/rails generate stimulus controllerName in our Rails application. This generates a file that looks like the following and also registers the controller in the file javascript/controllers/index.js.
// /javascript/controllers/alert_user_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
}
alertUser() {
alert("Hello user!");
}
}
The connect method is automatically generated, but could be omitted. This method is called whenever the page has finished loading and the controller is newly connected to an element on the page.
The controller can then be connected to an element on the page by using the data-controller attribute to the element. Every element nested inside an element with that attribute can then use the functions in the controller by using the data-action attribute. This could look like this:
// /views/alerts/index.html.haml
%div{"data-controller": "alert-user"}
%button{"data-action": "click->alert-user#alertUser"} Alert me!
Notice how the value of data-controller is alert-user even though the name of the controller is alert_user. This is because the controller name is also defined in the stimulus index file and by default they are hyphenated there.
Also good to know: "data-action": "click->alert-user#alertUser" can actually be shortened to"data-action": "alert-user#alertUser" because click is the default action for buttons. You can find the default actions for other elements here.
Turbo
Turbo provides us with several methods of dynamic page updates and navigation. The one we use here are Turbo Frames, which are explained in the next section.
Used frequently in our application (but not in the filter form) are Turbo Streams. Turbo Streams allow dynamic delivery of page updates directly from the controller (for example, after a form submission), ensuring a response to async actions. Turbo Streams have eight methods of altering a page, including update, delete, or morph.
For page navigation, Turbo gives us the power of Turbo Drive. Turbo Drive enhances page navigation and form submissions in non-single-page applications by turning full-page navigations or submissions into async fetch requests, and then rendering the HTML from the answer. Actions like updating browser history are also handled in the background.
Turbo Frames and async requests
One of the main technologies used in our filter form are Turbo Frames. Turbo Frames bring dynamic behaviour to static, backend-rendered Rails applications. To get started with Turbo Frames, declare one like this:
// /views/messages/index.html.haml
%turbo-frame{id: "message"}
%h3 I am a message on the index page
%link_to "Change me!", new_message_path
Here we have declared a Turbo Frame with the id message. Nested inside it, we find a link that leads to the edit page of the messages view. Let’s take a look at it.
// /views/messages/new.html.haml
%turbo-frame{id: "message"}
%h3 I am a message on the new page
%link_to "Go back!", messages_path
As we can see, the new page also contains a Turbo Frame with the id message. This is important because Turbo relies on the id to replace the Turbo Frame.
When you click on the link, Turbo prevents the default behavior, which means that the browser doesn’t follow the link. Instead, an async request the new action of the messages controller, to which Rails responds with HTML content. Turbo takes a look at this content and if found, extracts the corresponding Turbo Frame by comparing IDs. The original Turbo Frame on the view is then replaced with the fetched content, which leads to a partial page update.
Now let’s take a look at our original example, the PuzzleSkills filter form. Essentially, the form is split up in two parts. The search filter part (red) and the search result part (green).
Both are Turbo Frames. When the filter is updated, for example by adding a new filter row, Turbo makes a request and replaces the search filter Turbo Frame. When this happens, the form’s settings are appended to the query params. This allows us to extract the settings like the number of rows on render, which eventually leads to a visual update of the filter for the user.
Similarly, on every filter update Turbo also replaces the search result Turbo Frame with the fetched Turbo Frame, containing the search results, to show the results to the user.
Async requests
We’ve talked about async requests a few times now – but what are they exactly? When you normally visit a webpage, an HTTP request is sent to the server to initially load the page. This call is synchronous, which means you have to wait for the answer for the page to be loaded. But in our example, Turbo makes requests in the background to replace a Turbo Frame on the page without a full page reload. This is where async requests come into play. By using the global fetch() function, we are able to make a request from JavaScript to the server and then work with the response. This is an asynchronous request, because the execution is passed to a background worker, so the request runs independently and other code isn’t interrupted. Such requests are known as XHR requests, which stands for XMLHttpRequest.
Where and why JavaScript is still used
Sometimes, a functionality is needed where a Turbo Stream isn’t suitable, for example when there’s no interaction with a controller, or because additional actions are required. In these cases we normally use JavaScript by implementing a Stimulus controller.
In our example, we do this for the remove button that removes a filter row from the form.
The corresponding Stimulus controller looks like this:
// /javascript/controllers/people_skills_filter_controller import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["filter", "form"] remove({params}) { this.filterTargets[params.id].remove(); this.formTarget.submit(); } }
Notice the targets array. This is a functionality of Stimulus that allows us to get elements from the view by adding an attribute to an HTML tag with the following naming schema:
"data-<controller-name>-target": "<target-name>".
When the delete button is clicked, the remove method is executed, which first removes the correct filter from the DOM and then submits the form, leading to an update. We know which filter is the one to delete from the params. You can pass params to a Stimulus action by adding the following attribute to the element that calls the action:data-[identifier]-[param-name]-param.
Identifier is the Stimulus controller name. You can then either get the parameters from the event like this: e.params, or through destructured assignment as shown in our example.
In our case we have a loop in the template, so we just pass the index of the rendered filter row, which we then use as an index on the filterTargets array.
<target-name>Targets is automatically instantiated for every target in the targets array by Stimulus.
To find out more about how to integrate Stimulus controllers in your application, you could follow this short tutorial from the official Hotwire Stimulus documentation.