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.