I, too, am trying to perfect a “use modal for new/edit posts” pattern - and so far this is what I’ve got (and I do apologise for the mix of Erb/HAML templating). I like the FAB [fixed action button] so my ambition is to keep the posts#index in focus and inject @post into the modal. My posts_controller.rb
is almost identical to Doug Stull’s - so go enjoy his nice work!
In all of the following - one thing is unsolved (and one of the reasons I post this here, hoping to get some feedback telling me where I go wrong): when I click the ‘add’ fab button - the browser address line changes to /posts/new
. That’s all. Otherwise, I’m pretty happy with the functionality - but that URL rewrite really is a tough one to crack IMHO.
I tend to start my templating with a _body.html.erb and this one has a global script affording the opening of the modal from all over the place
# /app/views/application/_body.html.erb
<script>
// open recordModal
window.openRecordModal = function(){
rm = document.getElementById('recordModal')
rm.__x.$data.showModal = true
setTimeout(function(){ rm.querySelectorAll('input.autofocus')[0].focus() }, 250)
}
</script>
<body>
<main class="flex-1 relative overflow-y-auto focus:outline-none" tabindex="0">
<div class="py-6">
<%= yield %>
</div>
</main>
<%= yield :fab %>
</body>
The :yield
in the _body will make room for the posts/index template. I’ve skipped some of all the TailwindCSS - it’s totally cp’ed from TailwindUI anyway. I like AlpineJS for ‘sprinkling’ (maybe when I’m a bit more fluent in Stimulus I’ll go with that all the way)
# /app/views/posts/index.html.haml
:javascript
function fab(){
return {
show() { openRecordModal() }
}
}
function posts(){
return {
show() { openRecordModal() }
}
}
= turbo_stream_from "posts"
- content_for :fab do
.fixed-action-btn( x-data="fab()" )
%a( href="#{new_post_path}" x-on:click="show()" type="button" class="inline-flex...")
%i.material-icons add
= render "posts/modal", post: @post
= turbo_frame_tag "posts" do
= render @posts
The next template carries on where the previous ends - allowing for rendering each post.
# /app/views/posts/_post.html.erb
<%= turbo_frame_tag dom_id(post) do %>
<li class="md:flex justify-between items-center">
<p><a href="<%=edit_post_path(post)%>" x-on:click="show()" ><%= post.title %></a></p>
</li>
<li>
<hr class="my-5 border-t border-gray-300" />
</li>
<% end %>
Further up into the index template the modal template is called with render "posts/modal", post: @post
and that modal is next
<script>
function recordModal() {
return {
showModal: false,
open() { this.showModal = true },
close() { this.showModal = false },
is_open() { return this.showModal === true },
}
}
</script>
<!--Overlay-->
<div
id="recordModal"
x-data="recordModal()"
@keydown.escape="close()"
x-cloak
x-show="showModal"
:class="{ 'absolute inset-0 z-10 flex items-center justify-center': showModal }"
data-controller="modal"
data-modal-target="card"
class="overflow-auto"
style="background-color: rgba(0,0,0,0.5)"
>
<!--Dialog-->
<div @click.away="close()" class="bg-white..." >
<!--less important part of the modal -->
<%= render "form", post: @post %>
</div>
</div>
And finally the form
<%= turbo_frame_tag 'post_form' do %>
<%= form_with(model: post, id: dom_id(post), data: { "modal-target": "form", action: 'turbo:submit-end->modal#reset' }, class: "space-y-8 divide-y divide-gray-200" ) do |form| %>
<div class="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div class="sm:col-span-4">
<%= form.label :title, class: "block text-sm font-medium text-gray-700" %>
<div class="mt-1 flex rounded-md shadow-sm">
<%= form.text_field :title, class: "autofocus flex-1 focus:ring-indigo-500 focus:border-indigo-500 ck w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300" %>
</div>
</div>
</div>
<% end %>
<% end %>
Ups - I almost forgot the stimulus controller
import { Controller } from "stimulus"
export default class extends Controller {
static targets = ["card","form"]
reset() {
document.getElementById('recordModal').__x.$data.showModal = false
this.formTarget.reset()
}
}