How-To's Learn RubyMine Personal productivity RubyMine

How to use Stimulus in your Rails apps with RubyMine

Read this post in other languages:

Hello everyone!

The RubyMine team is constantly striving to provide support for new technologies for Ruby and Rails. One of the most exciting recent additions to Rails is undoubtedly Hotwire, so we’ve prepared an overview of this suite of frameworks and a tutorial on how to use the most important Turbo and Stimulus features in your Rails app with RubyMine. This post covers Stimulus; For Turbo, please see our previous blog post.

Hotwire and Stimulus

What is Hotwire?

Hotwire simplifies web development by sending HTML over the wire instead of JSON (hence the name, which stands for “HTML over the wire”). This reduces the amount of JavaScript developers need to write and the application needs to send to the browser, while also keeping template rendering on the server. Hotwire is made up of several frameworks: Turbo, Stimulus, and Strada. In this post, we’ll be looking at Stimulus.

What is Stimulus?

Stimulus is a JavaScript framework designed to work with static HTML and its existing DOM elements. It lets users add JavaScript functionality to the DOM by connecting elements to Stimulus controllers, which can be used to manipulate the DOM. Its goal isn’t to provide a full JavaScript frontend, but rather to enhance existing HTML elements.

The stimulus-rails gem is shipped by default with Rails 7, so you can start using it in your applications right away!

RubyMine provides support for Stimulus, such as code completion, navigation, and rename refactoring, which we encourage you to try as you make your way through the tutorial.

Tutorial: how to use Stimulus in Rails apps with RubyMine

In this tutorial, we’ll show you how to use the basic building blocks of Stimulus to easily integrate JavaScript into your applications. We’ll use a sample Rails application that allows the users to make accounts, create microposts, follow each other, and read the microposts in a feed.

Clone the sample Rails app

Follow these steps to clone and run our sample app:

  1. Check out the sample application at https://github.com/JetBrains/sample_rails_app_7th_ed/tree/hotwire_setup (make sure to switch to the hotwire_setup branch once you’ve cloned the project). For further information on how to set up a Git repository, please see the documentation.
  2. Specify the Ruby interpreter and install the gems.

Alice’s feed in the sample app

Let’s explore Stimulus in action by building a simple Copy to clipboard button.

Copy to clipboard button

Let’s add a Copy to clipboard button next to our Delete link. The intended function of this new button is to copy the text of the micropost when we click on it.

1. Open the _micropost.html.erb file and add the button to the view:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<% if current_user?(micropost.user) %>
<%= link_to "delete", micropost, data: { "turbo-method": :delete,
turbo_confirm: "You sure?" } %>
<% end %>
<%= button_tag "copy", class: "btn btn-link button-link-aligned" %>
<% if current_user?(micropost.user) %> <%= link_to "delete", micropost, data: { "turbo-method": :delete, turbo_confirm: "You sure?" } %> <% end %> <%= button_tag "copy", class: "btn btn-link button-link-aligned" %>
<% if current_user?(micropost.user) %>
  <%= link_to "delete", micropost, data: { "turbo-method": :delete,
                                           turbo_confirm: "You sure?" } %>
<% end %>

<%= button_tag "copy", class: "btn btn-link button-link-aligned" %>

To copy the text with a single mouse click, we’ll need to use JavaScript. To do that, let’s connect our button to a Stimulus controller.

2. Execute the following command using RubyMine’s Run Anything feature (Ctrl+Ctrl): rails generate stimulus clipboard

Generate Stimulus controller via Run Anything

This command will generate a Stimulus controller, which is the component that allows us to create a connection between our DOM elements and JavaScript.

The name of our controller is clipboard. This command generates a file named clipboard_controller.js in the app/javascript/controllers directory. The controllers from this directory are loaded automatically (see the file index.js).

3. In the file _micropost.html.erb, add the data-controller attribute:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<li id="micropost-<%= micropost.id %>" data-controller="clipboard">
...
</li>
<li id="micropost-<%= micropost.id %>" data-controller="clipboard"> ... </li>
<li id="micropost-<%= micropost.id %>" data-controller="clipboard">
  ...
</li>

This will allow us to connect our micropost to the clipboard controller. You can check if we successfully connected to the controller by adding some debug logging into the connect method and then checking the browser console upon loading the page.

4. Update the copy button in _micropost.html.erb:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<%=button_tag "copy", class: "btn btn-link button-link-aligned", data: { action: "clipboard#copy"} %>
<%=button_tag "copy", class: "btn btn-link button-link-aligned", data: { action: "clipboard#copy"} %>
<%=button_tag "copy", class: "btn btn-link button-link-aligned", data: { action: "clipboard#copy"} %>

When we click on the button, we want to perform a specific action, namely, copying some text. That text should come from a certain target, the body of the micropost. Actions and targets are some of the core concepts of Stimulus.

Actions connect controller methods to DOM events. When you click on an element, input some text, or submit a form, you can invoke some JavaScript code by specifying a controller method using the data-action attribute. This is exactly what we did with our button in the code above.

5. Add the copy method to the clipboard controller:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
export default class extends Controller {
connect() {
}
copy() {
}
}
export default class extends Controller { connect() { } copy() { } }
export default class extends Controller {
  connect() {
  }
  
  copy() {    
  }
}

To copy the text, we can use the navigator.clipboard.writeText method, except we need some way of obtaining the element from which the text is to be copied. This is where targets come in handy.

6. Add a target to your controller by pasting the following line into your controller class:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
static targets = ["source"];
static targets = ["source"];
static targets = ["source"];

Targets let you reference DOM elements that you might want to manipulate in your controller.

You can specify which targets your controller is going to be using inside a static field in the controller class.

7. Wrap the content of your micropost in a div tag in _micropost.html.erb:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<div data-clipboard-target="source">
<%= micropost.content %>
</div>
<div data-clipboard-target="source"> <%= micropost.content %> </div>
<div data-clipboard-target="source">
  <%= micropost.content %>
</div>

We can refer to controller targets inside our HTML by using the data-[controller-name]-target attribute, e.g. data-clipboard-target = "source".

8. Complete the body of the copy method:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// clipboard_controller.js
copy() {
navigator.clipboard.writeText(this.sourceTarget.textContent);
}
// clipboard_controller.js copy() { navigator.clipboard.writeText(this.sourceTarget.textContent); }
// clipboard_controller.js

copy() {
  navigator.clipboard.writeText(this.sourceTarget.textContent);
}

We can use this.sourceTarget to refer to the target inside our JavaScript controller.

Please note that navigator.clipboard is only available in secure contexts. For local development, you can run your app on localhost.

That’s it! Now we can copy the content of our microposts to the clipboard with a simple button click.

Hide a micropost from the feed

Other important Stimulus concepts include values, classes, and outlets.

You might want to pass some values as attributes of the controller element and use those values in your JavaScript code. This is exactly what Stimulus values are designed to do. Specify them next to the data-controller attribute as follows:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<li id="micropost-<%= micropost.id %>" data-controller="clipboard" data-clipboard-copyable-value="true">
<li id="micropost-<%= micropost.id %>" data-controller="clipboard" data-clipboard-copyable-value="true">
<li id="micropost-<%= micropost.id %>" data-controller="clipboard" data-clipboard-copyable-value="true">

Here, clipboard is the name of the controller, and copyable is the name of the property that we’d like to read (or write) in the controller. Then, you define the value names and types in the controller in a special static object, as shown below:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// clipboard_controller.js
static values = { copyable: Boolean };
// clipboard_controller.js static values = { copyable: Boolean };
// clipboard_controller.js

static values = { copyable: Boolean };

For further information about available types, please consult the documentation.

You can reference the values in your JavaScript code as this.[valueName]Value:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// clipboard_controller.js
if (!this.copyableValue) {
...
}
// clipboard_controller.js if (!this.copyableValue) { ... }
// clipboard_controller.js

if (!this.copyableValue) {
  ...
}

Outlets let you reference other Stimulus controllers and elements from within your controller: This way, different controllers in your project can communicate between themselves.

Classes refer to CSS classes: You can manipulate them programmatically from your JavaScript code. Let’s take a closer look at how to work with them by implementing a controller that can hide microposts on our feed with a single click. The same effect can be achieved by directly manipulating the style of the element (target.style.display = "none"), but we’ll accomplish this by setting the CSS class of the element instead. We can reuse this technique for many other visual effects, too.

1. Open the custom.scss file and add the following class:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
.invisible {
display: none
}
.invisible { display: none }
.invisible {
  display: none
}

2. Generate a controller to manipulate the visibility of posts: rails generate stimulus visibility.

3. Open the file visibility_controller.js and add a class and a target to the controller:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static classes = ["hidden"];
static targets = ["hide"];
}
import { Controller } from "@hotwired/stimulus" export default class extends Controller { static classes = ["hidden"]; static targets = ["hide"]; }
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static classes = ["hidden"];
  static targets = ["hide"];
}

4. Add a hide method to the controller:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
export default class extends Controller {
...
hide() {
this.hideTarget.classList.add(this.hiddenClass);
}
}
export default class extends Controller { ... hide() { this.hideTarget.classList.add(this.hiddenClass); } }
export default class extends Controller {
  ...
   
  hide() {
    this.hideTarget.classList.add(this.hiddenClass);
  }
}

This method will then append the CSS class hidden to the target hide.

5. Add the new controller and the class to the micropost view template by adding the following HTML attributes:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<li id="micropost-<%= micropost.id %>" data-controller="clipboard visibility" data-visibility-hidden-class="invisible" data-visibility-target="hide">
<li id="micropost-<%= micropost.id %>" data-controller="clipboard visibility" data-visibility-hidden-class="invisible" data-visibility-target="hide">
<li id="micropost-<%= micropost.id %>" data-controller="clipboard visibility" data-visibility-hidden-class="invisible" data-visibility-target="hide">

First, we add another controller: data-controller="clipboard visibility". Second, we specify which actual CSS class the logical class from the controller corresponds to: data-visibility-hidden-class="invisible". Classes must be specified on the same elements as the controllers to which they belong. Third, we specify the target that we want to hide, which is the entire micropost.

6. Add a hide button to the right of each micropost in _micropost.html.erb:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<span class="timestamp">
...
<%= button_tag "hide", class: "btn btn-link button-link-aligned", data: { action: "visibility#hide" }, style: "float: right;" %>
</span>
<span class="timestamp"> ... <%= button_tag "hide", class: "btn btn-link button-link-aligned", data: { action: "visibility#hide" }, style: "float: right;" %> </span>
<span class="timestamp">
  ...
  <%= button_tag "hide", class: "btn btn-link button-link-aligned", data: { action: "visibility#hide" }, style: "float: right;" %>
</span>

The button uses Stimulus actions to connect to the controller method hide.

If we click on this button, the corresponding micropost will disappear.

…until we refresh the page.

There are many ways to approach persisting the hidden state of a micropost here. For example, you could use Stimulus values and create a boolean value named hidden. Then, the hidden class will be automatically added to those elements for which the value is true. Alternatively, you could simply choose not to load such microposts from the database. To do that, though, we’ll need to communicate something back to our Rails app, which we can do by sending a simple request. Let’s revisit our hide method in the Stimulus controller.

7. Add the following code to the hide method in visibility_controller.js:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
hide(e) {
this.hideTarget.classList.add(this.hiddenClass);
const id = e.target.dataset.id
const csrfToken = document.querySelector("[name='csrf-token']").content
fetch(`/microposts/${id}/hide`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ hidden: true })
}).then(response => response.json())
}
hide(e) { this.hideTarget.classList.add(this.hiddenClass); const id = e.target.dataset.id const csrfToken = document.querySelector("[name='csrf-token']").content fetch(`/microposts/${id}/hide`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ hidden: true }) }).then(response => response.json()) }
hide(e) {
  this.hideTarget.classList.add(this.hiddenClass);
 
  const id = e.target.dataset.id
  const csrfToken = document.querySelector("[name='csrf-token']").content
  fetch(`/microposts/${id}/hide`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken
    },
    body: JSON.stringify({ hidden: true })
  }).then(response => response.json())
}

We’re now sending a very basic POST request to microposts/:id/hide. You could send anything in the body of the request, but here, we’ll simply state that hidden is now true.

Notice that this code uses the micropost ID, which it must receive from the view.

8. Pass the ID in the data hash next to the action in _micropost.html.erb:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<%= button_tag "hide",
class: "btn btn-link button-link-aligned",
data: { action: "visibility#hide", id: micropost.id },
style: "float: right;" %>
<%= button_tag "hide", class: "btn btn-link button-link-aligned", data: { action: "visibility#hide", id: micropost.id }, style: "float: right;" %>
<%= button_tag "hide",
              class: "btn btn-link button-link-aligned",
              data: { action: "visibility#hide", id: micropost.id },
              style: "float: right;" %>

9. Add the following route to routes.rb:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
post 'microposts/:id/hide', to: 'microposts#hide'
post 'microposts/:id/hide', to: 'microposts#hide'
post 'microposts/:id/hide', to: 'microposts#hide'

10. Add a hide method to microposts_controller.rb:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
def hide
@micropost = Micropost.find(params[:id])
hidden_post = HiddenPost.new
hidden_post.user = current_user
hidden_post.micropost = @micropost
hidden_post.save
end
def hide @micropost = Micropost.find(params[:id]) hidden_post = HiddenPost.new hidden_post.user = current_user hidden_post.micropost = @micropost hidden_post.save end
def hide
  @micropost = Micropost.find(params[:id])
  hidden_post = HiddenPost.new
  hidden_post.user = current_user
  hidden_post.micropost = @micropost
  hidden_post.save
end

11. Use Run Anything to generate a model that will store information about hidden posts for a specific user:

rails generate model HiddenPost user:references micropost:references

12. Update the User#feed method in the user.rb file:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# user.rb
def feed
following_ids = "SELECT followed_id FROM relationships
WHERE follower_id = :user_id"
hidden_posts_for_user = "SELECT micropost_id FROM hidden_posts WHERE user_id = :user_id OR user_id IN (#{following_ids})"
Micropost.where("user_id IN (#{following_ids})
OR user_id = :user_id", user_id: id)
.where("id NOT IN (#{hidden_posts_for_user})", user_id: id)
.includes(:user, image_attachment: :blob)
end
# user.rb def feed following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id" hidden_posts_for_user = "SELECT micropost_id FROM hidden_posts WHERE user_id = :user_id OR user_id IN (#{following_ids})" Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id", user_id: id) .where("id NOT IN (#{hidden_posts_for_user})", user_id: id) .includes(:user, image_attachment: :blob) end
# user.rb

def feed
  following_ids = "SELECT followed_id FROM relationships
                  WHERE  follower_id = :user_id"
  hidden_posts_for_user = "SELECT micropost_id FROM hidden_posts WHERE user_id = :user_id OR user_id IN (#{following_ids})"
  Micropost.where("user_id IN (#{following_ids})
                  OR user_id = :user_id", user_id: id)
          .where("id NOT IN (#{hidden_posts_for_user})", user_id: id)
          .includes(:user, image_attachment: :blob)
end

Now that we’ve saved the information about hidden posts in the database, those will no longer be visible in the user feed, even after a page refresh.

Hide button in action

Conclusion

In this tutorial, we’ve explored the Stimulus framework and its core concepts: controllers, targets, and actions. We’ve learned to use Stimulus in our Rails applications to easily connect our DOM elements to JavaScript.

Take advantage of Hotwire support in your favorite JetBrains IDE for Ruby and Rails. Download the latest RubyMine version from our website or via the free Toolbox App.

To learn about the newest features as they come out, please follow RubyMine on X.

We invite you to share your thoughts in the comments below and to suggest and vote for new features in the issue tracker.

image description