How to build a modal using Rails Hotwire, Tailwind, ViewComponents and Stimulus
I was recently tasked with this feature to create a modal in our Rails monolithic application at work. This was the acceptance criteria in the ticket:
Bit of background:
- We have an application where we show users their calendar. This calendar page shows slots according to the availability of the user.
- We would like to have a button on the Calendar page which opens a modal. In this modal, users would see their availability.
- Users should be able to edit their availability on this modal and the calendar page should update in realtime.
- We have assumed that the code to actually update the availability already exists and it works. We will just work around that to build our modal which is the primary focus of this task.
We’ll get how to do this in a bit but this is how the finished product looks like:
Clicking on the View/Edit availability opens a modal which looks like this:
Before we get into the details, I’ll briefly talk write about the technologies we are going to implement this with:
- Hotwire: Rails Hotwire includes Turbo frames which allows you to swap frames without reloading the entire page. We are going to use this to load our modal data into a frame whenever user clicks on the
View/Edit availabilitybutton. - Tailwind CSS: Tailwind is a utility based CSS framework which we are going to use to style our modals.
- ViewComponent: ViewComponent is a library which allows us to modularize our “view” code such that it can be re-used and tested well. Other added benefits are that it creates a well-defined interface for our “views” and has good performance benchmarks.
- Stimulus: Stimulus is this library from the folks at Basecamp which allows us to use minimal javascript only when needed.
Also, brief disclaimer that the code I am going to write in this post is just rough code which should give you an idea about how this works. It is not full-proof, complete and tested.
Step 1: Create Rails routes to view and edit the data in our modal
- Assumption:
You should have a Rails environment setup properly which is running and at least able to open the app in a browser.
All the gems should be installed for the technologies mentioned above.
- We will start with creating normal Rails routes for viewing and updating the availability. For now, we can consider that users will be taken to a new page whenever someone clicks on the
View / Edit availabilitybutton. - In order to make the routes RESTful, I am going to add a notion of “Weekly Availability” which is a list of availabilities for days in a week. So, the name of my controller is going to be
WeeklyAvailabilitiesControllerand we are going to have#showand#updateactions in the controller. - This is a rough draft of how the controller looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class WeeklyAvailabilitiesController < ApplicationController
before_action :authenticate_user!
def show
end
def update
if current_user.update!(update_params)
respond_to do |format|
// TODO: Redirect successful response
end
else
// TODO: Render unsuccessful response
end
end
private
def user_params
params.require(:user).permit(availabilities_attributes: [:id, :_destroy, :start_time, :end_time, :dow])
end
end
- Of course, we will also have standard code in our views and our routes file when we create a resource like
resources :weekly_availabilities, only: [:show, :update] - We will also hook this up with the
View/Edit availabilitybutton such that it should be linked with theweekly_availabilities#showaction. - However, important point is to note that the #show action opens in a different page or a new URL altogether when we click on the
View/Edit availabilitybutton.
Step 2: Move the #show action code to a ViewComponent
- So right now we have standard code in our
show.html.erb. This is the code which is automatically generated when we create a controller action. In this step, we will replace that code with a component. - We create a new component called
weekly_availability_component. This component will have code which displays the availabilities and also have a form where users can update their availabilities. - We basically copy the code from
show.html.erband paste it intoweekly_availability_component.html.erb. - However, using
view_componentgives us these added benefits:
We can now test our code by creating an instance of WeeklyAvailabilityComponent and assert how certain elements are going to look like on that page.
- We also have a clear separation where we pass variables to the component such as the
user. i.e. this component is tasked with the responsibility of showing and updating the availability of the passed in user. - We can now reuse this component on any other place in our app to achieve the same behavior.
This is how the view component will roughly look like:
1
2
3
4
5
6
7
8
9
10
11
// availability_modal_component.html.erb
<%= form_for current_user, method: :patch, url: weekly_availability_path(current_user), html: { id: 'availability_form', class: 'bg-white' }, data: { turbo: false } do |f| %>
// Code to view availabilities and also change availabilities
<%= f.submit 'Save' %>
<%- end %>
1
2
3
4
5
6
7
8
9
// availability_modal_component.rb
class AvailabilityModalComponent < ViewComponent::Base
attr_reader :current_user
def initialize(current_user:)
@current_user = current_user
end
end
- Few things to note here are:
We are usingdata: { turbo: false } to submit the form. This means we will not use Turbo for submitting the form and it will just be a regular whole page update.
- This can be modified to use Turbo. However, right now, for brevity, we have skipped that. We have also skipped the actual implementation of viewing and modifying the availability since it is not relevant for us right now.
Step 3: Adding a Turbo Frame so that the #show response opens on the same page.
- This is the most crucial part and something that is different or new to Rails. After implementing this, we should be able to see our
week_availabilities#showpage on our calendar page itself where we have the button toView/Edit availability - We start with declaring a
data: { turbo_frame: "weekly_availabilities_frame" }on ourView / Edit availabilitybutton. This will make sure that whenever we click this button, it will look forweekly_availabilities_framein the response to this request. It will swap thisweekly_availabilities_framewhich is already on this page with this same frame from the response. - Hence, we’ll need to also add a
weekly_availabilities_frameto the current calendars page where we would like to see the frame after it is loaded. - This can be done by simply adding a
turbo_frame_taglike this:
1
2
3
4
5
6
7
8
9
10
11
12
13
app/views/calendars/show.html.erb
// Other calendar#show view code
<%= link_to "View/Edit availability", weekly_availability_path(viewer), data: { turbo_frame: "modal" }, class: "btn btn-outline-primary" %>
<%= turbo_frame_tag "weekly_availabilities_frame" %>
// other calendar#show view code
- This would mean that the
weekly_availabilities_framefrom our response after clicking the button will be loaded in thediv.container - Now, we need to also add the same frame in our response as well. Our response is nothing but the HTML from
availability_modal_component.html.erb. We will just wrap that response with aturbo_frame_tag.
1
2
3
4
5
6
7
8
9
10
11
12
13
// availability_modal_component.html.erb
<%= turbo_frame_tag "weekly_availabilities_frame" do %>
<%= form_for current_user, method: :patch, url: weekly_availability_path(current_user), html: { id: 'availability_form', class: 'rounded-2xl bg-white h-full' }, data: { turbo: false } do |f| %>
// Code to view availabilities and also change availabilities
<%= f.submit 'Save' %>
<%- end %>
<%- end %>
- With this change, whenever we click on the button, it should display the
AvailabilityModalComponent’s html on the calendar page itself rather than opening on a new page with a different route.
Step 4: Adding a modal such that the Turbo Frame now opens in a dialog
- Now, we already have the turbo frame content loading on the same calendar page which has the
View/Edit availabilitybutton. Our next step is to load this turbo frame as a popup modal rather than directly embedded inside the calendar view. - To do this, we remove the
turbo_frame_tagwhich we have in theapp/views/calendars/show.html.erband rather put it in our layout file. - Let’s say our calendar view is loaded inside a layout which is located at
app/layouts/application.html.erb. We add thisturbo_frame_taginside our layout file below theyieldstatement.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<html>
<head>
<%= stylesheet_pack_tag 'application' %>
<%= stylesheet_link_tag 'fonts', media: 'all', 'data-turbo-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbo-track': 'reload' %>
<%= favicon_link_tag %>
<%= csrf_meta_tags %>
</head>
<body>
<main class="bg-white">
<%= yield %>
<%= turbo_frame_tag "weekly_availabilities_frame" %>
</main>
</body>
</html>
- With this change, we have just moved our
turbo_frame_tagfrom our view file to the layout file. It won’t still open as a popup. - To open it like a popup, we need to first make it look like a popup. This is where Tailwind comes in. Tailwind will make it look like a beautiful popup modal. We can go over to the Tailwind components and copy their custom code and paste it in out layout file inside the container class. I am going to be using the components from Tailwind UI but we can use other modals as well.
- With this change, there will be a few divs and classes added which wrap the
turbo_frame_tagin the application layout html file. - This will make it look like a popup. Now, next step is to actually open the popup when the turbo frame loads. We are going to use a little bit of stimulus for this step.
- Using stimulus, we will use some javascript to open the turbo frame content in a modal.
- We will first replace our container element by a <
dialog> element. If we call methods such asshowModalon this HTML element, it will open the containing HTML as a popup modal. - Now, we just have to call these methods whenever the turbo frame loads. For this, we use a stimulus controller which will have the dialog as a target.
- Inside the stimulus controller, we will have methods to
openandclosethe modal. This is how the Stimulus controller looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["dialogPopup"]
open(event) {
event.preventDefault();
this.dialogPopupTarget.showModal();
}
close(event) {
event.preventDefault();
this.dialogPopupTarget.close();
}
}
- Now, we need to call the #open method whenever a frame is loaded. We need to call #close whenever the user presses the cancel button in the modal or if user updates the availability. i.e. user updates the form.
- Turbo provides us with certain events which are called during the lifecycle of a turbo frame load operation. We have a method called
turbo:frame-loadwhich is called whenever a frame completely loads. We have another event called turbo:submit-end. - We call the
#openmethod in theturbo:frame-loadcallback. We call the#closemethod in theturbo:submit-endcallback and also in thelink_tomethod of theCancelbutton in the modal. - This layout file now looks somewhat like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<html>
<head>
<%= stylesheet_pack_tag 'application' %>
<%= stylesheet_link_tag 'fonts', media: 'all', 'data-turbo-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbo-track': 'reload' %>
<%= favicon_link_tag %>
<%= csrf_meta_tags %>
</head>
<body>
<main class="bg-white">
<%= yield %>
modal#open turbo:submit-end->modal#close"
data-modal-target="container"
>
<dialog data-modal-target="dialogPopup">
// Tailwind divs for showing a modal
<%= turbo_frame_tag "weekly_availabilities_frame" %>
</dialog>
</main>
</body>
</html>
- With this change, our turbo frames will load as a popup whenever user clicks on the
View/Edit availabilitybutton. The popup will close whenever the user submits the form on the modal or closes the modal by pressingCancel
Phew! That’s it! We have now built a modal using Rails Hotwire Turbo frames, Stimulus, ViewComponents and TailwindCSS. Please let me know in the comments if anything is unclear or if you have any ideas or suggestions.


