Reload page with turbo frame after modal form submit

Hello, I have a form inside a modal and the whole modal content is a turbo-frame element with an ID.

It’s working fine because when there are validation errors, the modal is re-rendered seamlessly with the error messages.

The problem is when the form is submitted successfully. Since I am redirecting to the same page with a success flash message, the modal is still in the page with the same ID and so all I get is a new empty modal rendered.

I would like, instead, to reload the page to show the flash message and show the success flash when the form is submitted successfully. I don’t want to always use the target="_top" because I’m fine with the modal reloading only the errors. I tried with the meta element rendered when form is submitted successfully but it didn’t work.

Is there a way to achieve this or I should re-factor my page and my form?

Thanks.

You may be interested in this thread

Thanks @tleish, really interesting discussion. I asked more info to elik-ru because he seemed to have the exact same issue as me and he was able to resolve with an additional frame. Unfortunately I didn’t understand the exact process.

I answered there :slight_smile: My solution broke in last turbo updated, but there is a workaround

Here is another solution to this problem (that works with the latest version of Turbo) that uses a turbo stream to perform a Turbo.visit: https://github.com/hotwired/turbo-rails/pull/367#issuecomment-1601733561

Hi @lukepass . Can you please share what’s the solution you implemented to solve this?

I never actually solved it, my modals never close and just show a success message inside when the submit is enabled.

I had a similar problem where I wanted to close a modal which is reused, and refresh a related turbo_frame element. I’ll put it here in the hopes it’s helpful to someone.

In the controller

  # POST /oroshi/supplies
    def create
      @supplier = Oroshi::Supplier.new(supplier_params)
      if @supplier.save
        head :ok
      else
        render partial: 'modal_form', status: :unprocessable_entity
      end
    end

the modal is reused it has a turbo frame for the contents, that way i can reuse this for various new records for different models:

parent:

<div data-controller="oroshi--dashboard">

  <%= render 'oroshi_modal' %>

  <h3 class="text-center mb-4">供給受付設定</h3>

oroshi_modal:

  <div class="modal fade" id="oroshiModal" tabindex="-1" data-oroshi--dashboard-target="modal">
    <div class="modal-dialog">
      <%= turbo_frame_tag 'oroshi_modal_content', class: 'modal-content' do  %>
        <div class="modal-header">
          <h5 class="modal-title">タイトル</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body">
          <p>内容はこちら</p>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">閉じる</button>
          <button type="button" class="btn btn-primary">保存</button>
        </div>
      <% end %>
    </div>
  </div>

example of modal form placed in ‘oroshi_modal_content’

<%= turbo_frame_tag 'oroshi_modal_content', class: 'modal-content' do  %>
  <div class="modal-header">
    <h5 class="modal-title">新しい組織を作成する</h5>
    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  </div>
  <div class="modal-body">
    <%= render 'modal_form' %>
  </div>
<% end %>

and the form rendered here:

<%= turbo_frame_tag 'supplier_modal_form' do %>
  <%= simple_form_for @supplier, 
                      defaults: { input_html: { class: 'mb-2' } },
                      data: {
                        controller: 'oroshi--suppliers--representatives',
                        action: 'oroshi--dashboard#modalFormSubmit:prevent',
                        refresh_target: 'suppliers',
                      } do |f| %>
    <% if @supplier.errors.any? %>
      <div id="error_explanation">
        <h2><%= pluralize(@supplier.errors.count, "error") %> この生産者の保存を禁止しました</h2>

        <ul>
        <% @supplier.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
        </ul>
      </div>
    <% end %>
    <%= f.label :supplier_organization_id, t('simple_form.labels.oroshi_supplier.supplier_organization_id') %>
    <%= f.collection_select :supplier_organization_id, @supplier_organizations, :id, :organization_name, { selected: @supplier.supplier_organization ? @supplier.supplier_organization.id : @supplier_organizations.first.id }, { class: 'form-control mb-2' } %>
    <%= f.input :company_name, as: :string %>
    <%= f.input :supplier_number %>
    <%= f.input :address, as: :string %>
    <div id="representatives">
      <% representatives = @supplier.representatives.present? ? @supplier.representatives : [''] %>
      <% representatives.each do |representative| %>
        <div class="representative"> 
          <%= f.label :representatives, t('simple_form.labels.oroshi_supplier.representatives') %>
          <%= f.text_field :representatives, multiple: true, value: representative, class: "form-control mb-2" %>
        </div>
      <% end %>
    </div>
    <div class="d-flex justify-content-end align-items-end align-middle">
      <%= button_tag type: 'button', 
                    class: 'btn btn-sm btn-secondary remove-representative',
                    data: {
                      action: 'click->oroshi--suppliers--representatives#removeRepresentative:prevent'
                    } do %>
        <%= icon('dash') %>
      <% end %>
      <%= button_tag type: 'button', 
            class: 'btn btn-sm ms-2 btn-secondary add-representative',
            data: {
              action: 'click->oroshi--suppliers--representatives#addRepresentative:prevent'
            } do %>
        <%= icon('plus') %>
      <% end %>
    </div>
    <%= f.input :home_address, as: :string %>
    <%= f.input :phone %>
    <%= f.input :invoice_number, as: :string %>
    <div class="form-check form-switch">
      <%= f.check_box :active, class: 'form-check-input', id: 'activeSwitch', checked: true  %>
      <%= f.label :active, class: 'form-check-label', for: 'activeSwitch' %>
    </div>
    <div class="form-check form-switch">
      <%= f.check_box :invoice_as_organization, class: 'form-check-input', id: 'invoiceSwitch', checked: true %>
      <%= f.label :invoice_as_organization, class: 'form-check-label', for: 'invoiceSwitch'  %>
    </div>
    <%= f.button :submit,
                  '保存',
                  class: 'btn btn-primary my-2' %>
  <% end %>
<% end %>

Then the stimulus controller for oroshi–dashboard calls a function modalFormSubmit, interrupting the normal submit process with :prevent, performs the following:


  modalFormSubmit(event) {
    // submit the form referenced by the event target, via fetch
    const form = event.target;
    const modal = bootstrap.Modal.getOrCreateInstance(this.modalTarget);
    const url = form.action;
    const method = form.method;
    const refreshTarget = form.dataset.refreshTarget
    const formData = new FormData(form);
    fetch(url, {
      method: method,
      body: formData
    })
      .then(response => {
        if (response.ok) {
          modal.hide();
          const frame = document.querySelector(`turbo-frame#${refreshTarget}`);
          frame.reload();
        } else {
          return response.text();
        }
      })
      .then(html => {
        if (html) {
          this.modalTarget.querySelector('.modal-body').innerHTML = html;
        }
      });
  }

Now, when the form submits if there’s an error it returns with the errors in the modal by replacing the form, but when the response is 200 it closes the modal and refreshes the relevant turbo_frame.

Here’s what it looks like, BTW (sorry all Japanese):


Here’s the shown modal with the form for Suppliers in it:

also not sure about this line because I should probably be using turbo to replace the frame: this.modalTarget.querySelector('.modal-body').innerHTML = html;
but hey, it works.