How to use modals with forms in Rails using Turbo
Tags: rails, turbo, stimulus, hotwire
Published: 2021-01-08 05:04:09

---
title: How to use modals with forms in Rails using Turbo
published: true
description: Handling form submissions with modals can be tricky, especially with the new Hotwire/Turbo framework.
tags: rails, turbo, stimulus, hotwire
//cover_image: https://direct_url_to_image.jpg
---
# Goal
Using the [Hotwire](https://hotwire.dev/), 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](https://gorails.com/).  We'll assume some familiarity with [Stimulus](https://stimulus.hotwire.dev/) in order to keep focused on the benefits of [Turbo](https://turbo.hotwire.dev/).

[Source code for this guide - updated to turbo-rails 0.5.9](https://gitlab.com/doug.stull/turbo_modal/)

## Posts page being referenced

![Screen Shot 2021-01-07 at 11.21.43 PM](https://dev-to-uploads.s3.amazonaws.com/i/t6g29yw70dndt1565baq.png) 

## Creating a new post using the modal

### New link and modal setup

**Note** [this github comment](https://github.com/hotwired/turbo-site/pull/40#discussion_r578763199) is what I used as guide for this setup.

This all starts in [index.html.erb](https://gitlab.com/doug.stull/turbo_modal/-/blob/master/app/views/posts/index.html.erb)

```html
<%= 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 tell `turbo` that links with a matching `data-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 tell `turbo` to leave the url unchanged when the link is clicked).  Our action upon `New Post` click will be a replace that is defined in the [new.html.erb](https://gitlab.com/doug.stull/turbo_modal/-/blob/master/app/views/posts/new.html.erb) template. This will populate the modal via use of the `target` value on the `turbo-stream` element in the template.
- render our `modal_form` partial which will contain the modal definition and define a `div` with an `id` that the `turbo-stream` response replaces.
- `data-controller="post-modal"` names our `post-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 fetch `new_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](https://gitlab.com/doug.stull/turbo_modal/-/blob/master/app/controllers/posts_controller.rb).

```ruby
def new
  @post = Post.new
end
```

- We render the html response of [new.html.erb](https://gitlab.com/doug.stull/turbo_modal/-/blob/master/app/views/posts/new.html.erb)

```ruby
<%= 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 the `id` inside the modal.  This will then replace that element inside the modal with our form content. 
- Below is an example response

 ![Screen shot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gtv0hxx1hm1tkobtlabo.png)

- 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](https://gitlab.com/doug.stull/turbo_modal/-/blob/master/app/javascript/controllers/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](https://gitlab.com/doug.stull/turbo_modal/-/blob/master/app/javascript/controllers/post_modal_controller.js)'s open function.
   - this is extended from the original modal supplied from [tailwindcss-stimulus-components](https://github.com/excid3/tailwindcss-stimulus-components)

```js
document.addEventListener("postForm:load", () => {
  this.containerTarget.classList.remove(this.toggleClass);
});
```

- The above is done in order to take advantage of the [connectCallback](https://github.com/hotwired/turbo/blob/aae160bed764ca7322be1e3a9e00366787d96f7f/src/elements/stream_element.ts#L7) that seems to be the only way to know when the `replace` action from `turbo-stream` is [completed](https://github.com/hotwired/turbo/blob/aae160bed764ca7322be1e3a9e00366787d96f7f/src/elements/stream_element.ts#L77).

### Rendering the response

At this point the modal that is defined in [_modal_form.html.erb](https://gitlab.com/doug.stull/turbo_modal/-/blob/master/app/views/posts/_modal_form.html.erb) is opening and this element below is being replaced with the `turbo-stream` response that has the rendered form.

```html
<%= tag.div nil, id: 'post_form' %>
```

![Screen Shot 2021-01-07 at 11.34.34 PM](https://dev-to-uploads.s3.amazonaws.com/i/9bd1cs8lxie09wozuoqr.png)
 
### 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](https://gitlab.com/doug.stull/turbo_modal/-/blob/master/app/controllers/posts_controller.rb)

```ruby
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)

![Screen Shot 2021-01-07 at 11.44.33 PM](https://dev-to-uploads.s3.amazonaws.com/i/2yavtdv5r7sd8ofk2iwa.png)

## Editing a post using the modal 

This uses the same concepts as the new submission and starts off in [index.html.erb](https://gitlab.com/doug.stull/turbo_modal/-/blob/master/app/views/posts/index.html.erb) as seen below.

```html
<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](https://gitlab.com/doug.stull/turbo_modal/)

## 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.