Doug Stull
Posted on January 8, 2021
Goal
Using the Hotwire, show how to setup a basic implementation of a modal with a form.
We'll break this down into talking about the controller, view templates and javascript.
There are many other things that were setup in the example application due to the choices made in the tech stack and some setup/configuration has been derived from things learned on Go Rails. We'll assume some familiarity with Stimulus in order to keep focused on the benefits of Turbo.
Source code for this guide - updated to turbo-rails 0.5.9
Posts page being referenced
Creating a new post using the modal
New link and modal setup
Note this github comment is what I used as guide for this setup.
This all starts in index.html.erb
<%= turbo_frame_tag 'post' %>
<div class="mt-8" data-controller="post-modal" data-post-modal-prevent-default-action-opening="false">
<%= render partial: 'posts/modal_form' %>
...
<%= link_to 'New Post', new_post_path, class: "btn btn-primary", data: { action: "click->post-modal#open", 'turbo-frame': 'post' } %>
-
turbo_frame_tag
allows us to tellturbo
that links with a matchingdata-turbo-frame
value matching it will return an html response including a matching frame tag(not sure on this wording, as we supply it here to merely tellturbo
to leave the url unchanged when the link is clicked). Our action uponNew Post
click will be a replace that is defined in the new.html.erb template. This will populate the modal via use of thetarget
value on theturbo-stream
element in the template. - render our
modal_form
partial which will contain the modal definition and define adiv
with anid
that theturbo-stream
response replaces. -
data-controller="post-modal"
names ourpost-modal
controller and causes it to connect on render. -
data-post-modal-prevent-default-action-opening="false"
will allow us to open the modal with the link and also fetchnew_post_path
from the controller and render our modal content.
Clicking the New Post
link
This action will take us into the posts_controller.rb.
def new
@post = Post.new
end
- We render the html response of new.html.erb
<%= turbo_frame_tag 'post' do %>
<turbo-stream target="post_form" action="replace">
<template>
<%= render partial: "posts/form", locals: { post: @post } %>
</template>
</turbo-stream>
<% end %>
- We render the
turbo_stream
response inside the template with a target that matches theid
inside the modal. This will then replace that element inside the modal with our form content. - Below is an example response
- While the data is being fetched, the modal is given the signal to open, so we'll listen for an event from another Stimulus controller. That controller is the post_form_controller.js, and it keeps us from seeing unprepared html and stale data from an invalid form submission.
- We can achieve that by adding an
eventListener
to post_modal_controller.js's open function.- this is extended from the original modal supplied from tailwindcss-stimulus-components
document.addEventListener("postForm:load", () => {
this.containerTarget.classList.remove(this.toggleClass);
});
- The above is done in order to take advantage of the connectCallback that seems to be the only way to know when the
replace
action fromturbo-stream
is completed.
Rendering the response
At this point the modal that is defined in _modal_form.html.erb is opening and this element below is being replaced with the turbo-stream
response that has the rendered form.
<%= tag.div nil, id: 'post_form' %>
Submitting the form
When we submit the form, the modal closes via close
function in the extended post-modal
stimulus controller and reach the create
method in the posts_controller.rb
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.html { redirect_to posts_url, notice: 'Post was successfully created.' }
else
format.turbo_stream do
render turbo_stream: turbo_stream.replace('post_form',
partial: "posts/form",
locals: { post: @post })
end
end
end
end
- If successful, we'll just redirect to the page the modal is launched from, and flash a message. This will all feel seamless since Turbo takes care of replacing items in place; negating the need for a full page reload.
- When the form submission is not successful,
turbo_stream
format is rendered and the form is replaced(leaving the modal open and adding in the errors automatically)
Editing a post using the modal
This uses the same concepts as the new submission and starts off in index.html.erb as seen below.
<div class="align-middle min-w-full overflow-x-auto shadow overflow-hidden sm:rounded-lg" data-controller="post-modal" data-post-modal-prevent-default-action-opening="false">
<table class="min-w-full divide-y divide-cool-gray-200">
<thead>
<tr>
<th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-cool-gray-500 uppercase tracking-wider">Title</th>
<th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-cool-gray-500 uppercase tracking-wider">Body</th>
<th colspan="1" class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-cool-gray-500 uppercase tracking-wider"></th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-cool-gray-200">
<% @posts.each do |post| %>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm leading-5 text-cool-gray-900">
<%= post.title %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm leading-5 text-cool-gray-500">
<%= post.body %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm leading-5 text-cool-gray-500">
<%= link_to 'Edit', edit_post_path(post), class: "btn btn-secondary", data: { action: "click->post-modal#open", 'turbo-frame': 'post' } %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
The rest of edit and delete can be found in the example application
In closing
Disclaimer: The code above is far from perfect
In some places the implementation could definitely use refinement(feel free to suggest an improvement).
I chose a modal here as I had that particular issue to solve on a project and didn't see a ready-made solution/guide.
Overall I am happy with what Turbo enables and appreciate how the hard work of others to build these types of things, make it easier for everyone else to produce things quickly.
Posted on January 8, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.