This tutorial has an update using Turbo Streams and Stimulus. You can read it here: Stimulus.js & HOTWire Tutorial: Interactive Deletes
Interactive websites have that feeling of immediacy. Clicked links respond in milliseconds, and there is never a need to wait… Waiting for a remote record to delete, and then the whole page refresh afterwards can feel like an eternity in web’s modernity. But, A little Stimulus works well with Rails’ Unobtrusive Javascript to make items on a page disappear immediately, all while a network request happens in the background. This tutorial takes a remote deletion link, adds an in-place confirmation message, and then hides the element when the delete request is sent to the server. When the request comes back with a success, the element is removed from the page, or with an error, the element put right back in place.
The controller listens for a few different events emitted by UJS when the delete link is clicked. It stops the remote request the first time the link is clicked, and puts a confirmation message in the link. This feels better than the usual full alert modal that adding confirmation message usually throws up. Clicking the link the second time lets the request go back to the server. The element’s style is then set to display:none;
so that it disappears immediately. A successful response removes the element entirely, and an unsuccessful response reverts the style, so the element appears again. A timeout is added after the first click to reset the state of the controller and the link if it isn’t clicked a second time.
The HTML
I’m working with a simple ActiveRecord Post
model which has a title, author, and text. The controller is similar to what you could generate from the Rails scaffold command. On the index.html.erb
view, this is the HTML:
<h1 >All Posts</h1>
<%= link_to "New Post", new_post_path %>
<% @posts.each do |post| %>
<div data-controller="delete">
<p>
<h2><%= link_to post.title, post %></h2>
<strong><%= post.author %></strong>
<br />
<em><%= post.created_at.strftime("%l:%M %P") %></em>
<br />
<%= link_to "Delete", post_path(post), method: :delete, remote: true, data: { target: "delete.link", action: "ajax:beforeSend->delete#click" } %>
</p>
</div>
<% end %>
There is a div
element for the delete controller. The delete link towards the bottom is a target of the delete controller, since the controller needs to manipulate it. The controller also listens for the ajax:beforeSend
event so that the controller can add our visual interactivity. The controller doesn’t need to change anything else with how Rails sends and receives the delete request, which is great, as that’s less work for the Stimulus controller and fewer chances for bugs.
The Stimulus Controller
The delete_controller.js
Stimulus controller is the component in charge of the delete actions. It keeps track of the click state, changes the link text, and handles the result of the server’s response.
On connect()
, the controller sets its delete state to false. This dictates later on how the click handler behaves.
On a call to click(event)
when delete is false, the controller records the current delete link text, changes the delete link to a new confirmation message, sets a reset timeout, and then stops the click event. On a call to click(event)
when delete is true, the controller hides its element, and adds listeners for the server response. The response proceeds back to the server as usual.
The handleSuccess(event)
method removes the element from the page, and by extension, disconnects the controller.
The handleError(event)
method resets the element back to the way the element looked, and makes the element visible again.
The resetState()
method removes the event listeners, sets the delete link back to the original test, and resets the delete state back to false.
const RESET_TIMEOUT_MILLIS = 3000;
const CONFIRMATION_MESSAGE = '<strong>Are you sure?</strong>';
import { Controller } from "stimulus"
export default class extends Controller {
static targets = ['link']
connect() {
this.delete = false;
}
click(event) {
if (this.delete) {
this.element.style = 'display: none;';
this.linkTarget.addEventListener('ajax:success', this.handleSuccess.bind(this))
this.linkTarget.addEventListener('ajax:error', this.handleError.bind(this))
} else {
this.oldMessage = this.linkTarget.innerHTML;
this.linkTarget.innerHTML = CONFIRMATION_MESSAGE;
this.delete = true;
this.timeout = setTimeout(() => {
this.resetState();
}, RESET_TIMEOUT_MILLIS);
event.preventDefault();
return false;
}
}
handleSuccess(event) {
clearTimeout(this.timeout);
this.element.parentNode.removeChild(this.element);
}
handleError(event) {
clearTimeout(this.timeout);
this.resetState();
this.element.style = '';
}
resetState() {
if (this.delete) {
this.linkTarget.removeEventListener('ajax:success', this.handleSuccess.bind(this))
this.linkTarget.removeEventListener('ajax:error', this.handleError.bind(this))
this.linkTarget.innerHTML = this.oldMessage;
this.delete = false;
}
}
}
Practice?
Try to add something to alert the deleter on the page when the deletion fails. It can assume that failure isn’t a likely case, so the error message should be very disruptive to the page flow.
Interactivity
By making actions on a web page feel immediate, web app users feel more confident that the app registered their wishes. Deletion is definitely a good spot for double checking about someone’s intention, but by changing the delete link text, instead of an alert, it’s not as disruptive as usual. Imagine having to delete hundreds of records, and having to move the cursor multiple times for each action, and you’ll see this method is better for everyone.
Comments or Questions? Let me know how your experience went below.
Want To Learn More?
Try out some more of my Stimulus.js Tutorials.
5 comments on “Stimulus.js Tutorial: Interactive Deletes with Rails UJS”
Really enjoying your tutorials.
When you remove event listeners with `Element.removeEventListener()` you hate to provide exactly the same function. But using `Function.bind()` method give you absolutely new function. So event listener in your code will not be removed.
Bind functions in `contructor()` like this:
““
constructor(context) {
super(context);
this.handleSuccess = this.handleSuccess.bind(this);
// …
}
“`
Or use arrow functions when declaring event handlers in class
“`
successHandler = event => {
// …
}
Respectfully yours
Typo, instead “hate to” I mean “have to”. Sorry 🙁
Great tutorial. Had to change the view from link_to to use a button_to for this to work 🙂
[…] Try out some more of my Stimulus.js Tutorials. This is an update of previous tutorial that used Rails UJS, Stimulus.js Tutorial: Interactive Deletes with Rails UJS. […]