I have a page with several modal forms for filling out academic data. For each modal, the relevant Rails controller actions render turbo responses for the form in a separate modal.
Note: this does not follow the pattern of populating a modal that is pre-printed in the page, because in this app, the user may need to have several modals in progress at once.
So I'm doing it this way because I want users to be able to work on more than one form at a time, before submitting any of the forms. For example, they can close a modal to consult the page underneath, or consult a different modal form in progress. Then they can re-open the first modal again with the same toggle on the page, and pick up where they left off.
Each modal is only rendered from scratch the first time it appears, and then it can be closed or opened, without removing it from the DOM, until it's submitted.
I'm currently handling this with a Stimulus controller that checks if the modal is already in the DOM, and either show
s the existing modal, or fetch
es a response from the Rails controller using requestjs-rails
, to render the blank form modal and put it in the DOM, and show
it.
Having just discovered Turbo.StreamActions
, i'm wondering if this can be accomplished entirely within the Turbo response.
But I can't imagine how to check the DOM state (specifically, return a boolean whether there's a modal with the same ID already printed in the page) with a Turbo custom action: Turbo actions seem to be a one way response from Ruby --> JS. They are for sending "HTML over the wire." The "payload" of a Turbo.StreamAction is html, not data.
or... can I write a custom Turbo.StreamAction
to return
a value? I can't picture how it would work... and doing something like this seems to violate the intended separation of scope between Turbo and Stimulus.
Here's where i'm stuck:
# app/views/somethings/new.erb
# can a turbo_stream.action take two arguments?
<%
args = { partial: "shared/modal_form",
locals: { title: title, body: body } }
<%= turbo_stream.show_or_render_modal_form(id, args) %>
// app/javascript/application.js
Turbo.StreamActions.open_modal = function () {
$("#" + this.templateContent.textContent).modal('show')
};
Turbo.StreamActions.modal_exists = function () {
return document.getElementById(this.templateContent.textContent)
};
Turbo.StreamActions.show_or_render_modal_form = function () {
// ??? need to pass an id for the form, the partial path,
// and local_assigns for the partial. Can that all be
// packed into this.templateContent ?
};
# config/initializers/turbo_stream_actions.rb
def show_or_render_modal_form(id, args)
if action(:modal_exists, "#", id)
action(:open_modal, "#", id)
else
action(:render, "#", **args)
end
end
There is no need for Stimulus here, but the "render or show modal" part should be handled from javascript.
When you click on these buttons, it should first render a text field, second click will just reveal rendered modal and hide all the other modals, any changes in the text fields should persist.
Obviously, I've skipped the actual modal css look, that will depend on your front end css framework.
https://github.com/hotwired/turbo/blob/v7.3.0/src/elements/stream_element.ts
https://github.com/hotwired/turbo-rails/blob/v1.5.0/app/models/turbo/streams/tag_builder.rb
If you need a more complicated logic, you could render a
<turbo-stream>
tag with other attributes that can be used in javascript:https://github.com/hotwired/turbo-rails/blob/v1.5.0/app/helpers/turbo/streams/action_helper.rb#L26
But, maybe at this point it would be simpler to have a stimulus controller for the front end logic.