Rails: What's new slide out drawer
Source code can be found here: https://github.com/jespr/rails-whats-new-slide-out
Create a new announcement model, for this example we’ll just use the Rails scaffold functionality since this isn’t really the focus of the tutorial: rails g scaffold announcement title body announcement_type
- this will give us the controller with CRUD functionality and the model.
I’m using action_text for the announcement body, but you can leave that out. In order for this to work we need to install action_text in our Rails app if we don’t have it already:
bin/rails action_text:install
Run the migrations rails db:migrate
Let’s go ahead and create a new announcement by visiting http://localhost:3000/announcemens/new and fill in a test title and description (we can set the announcement type to new
for now)
Let’s make a quick change to what an announcement looks like. Open app/views/announcement/_announcement.html.erb
And change it to this:
<div id="<%= dom_id announcement %>">
<p class="my-5">
<h2 class="font-bold text-2xl text-gray-900"><%= announcement.title %></h2>
</p>
<p class="my-5">
<%= announcement.body %>
</p>
</div>
We want a little bit more spacing between the announcements when you have multiple, so open app/views/announcements/index.html.erb
and add space-y-12
to the div
the announcements are rendered within so it looks like this:
<div id="announcements" class="min-w-full space-y-12">
<%= render @announcements %>
</div>
Time for the fun part
Let’s make so when you click on a “What’s new” link inside of your product we slide out a drawer on the right side with a list of the announcements.
Let’s go ahead and add a link somewhere. In my case I have a dashboard view, so I’m going to add it to that:
<%= link_to "What's new?", announcements_path, data: { turbo_stream: true } %>
When we click this link it’ll make a TURBO_STREAM request to the announcement_controller’s index action.
In order for that to respond correctly, we want to create the following file app/views/announcements/index.turbo_stream.erb
And that file will have the following:
<%= turbo_stream.replace :announcements, partial: "announcements/announcements", locals: { announcements: @announcements } %>
What that is doing is that it replaces the content of the turbo_frame named :announcements
with the content of the announcements/announcements
partial. So let’s go ahead and create that partial in app/views/announcements/_announcements.html.erb
With the following:
<%= turbo_frame_tag :announcements do %>
<%= render "announcements_list", announcements: @announcements %>
<% end %>
Let’s make a small refactor in our app/views/announcements/index.html.erb
file. Let’s move this:
<div id="announcements" class="min-w-full space-y-12">
<%= render announcements %>
</div>
Out into it’s own partial, so replace the above with:
<%= render “announcements_list”, announcements: @announcements %>
And add it to this new file in app/views/announcements/_announcements_list.html.erb
:
<div id="announcements" class="min-w-full space-y-12">
<%= render announcements %>
</div>
We now have the announcements list that we can use in both our index.html.erb
and index.turbo_stream.erb
files. Even though for the turbo_stream template it’s referenced from inside of the announcements
partial so we can properly wrap it in a turbo_frame_tag
.
The last little thing we need to do is add a turbo_frame_tag
with the name of :announcements
to our layout. We’ll just go ahead and add it right before the closing body tag in app/views/layouts/application.html.erb
:
<body>
<main class="container mx-auto mt-28 px-5 flex">
<%= yield %>
</main>
<%= turbo_frame_tag :announcements %>
</body>
If you go refresh the page where you added the “What’s new” link you should see the list of announcements being rendered at the bottom of your page when you click it.
Let’s add the drawer
But we want it to by shown in a drawer on the right side? So let’s go ahead and add that functionality now. In our app/views/announcements/_announcements.html.erb
file, wrap the announcements_list
render line in a div
, like this:
<%= turbo_frame_tag :announcements do %>
<div class="fixed bg-white rounded max-w-lg p-4 right-3 top-3 bottom-3 shadow-md border overflow-y-scroll ">
<%= render "announcements_list", announcements: @announcements %>
</div>
<% end %>
We’ve added a div, with position fixed
, given it a white background, made it rounded, given it a max width, some padding and inset it from the top, bottom and right side. We’ve also made it so if the content inside of it is larger than the viewport, then we can scroll it - and we’ve given it a shadow and a border to stand out from the content behind it - but you can of course do whatever you want :)
Make it so we can close it
We want to be able to close the drawer after you have clicked the link, without having to refresh your browser. This is where Stimulus.js comes in. Let’s create a new stimulus controller: rails g stimulus announcements
- we want that to contain the following:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
close() {
this.element.remove()
}
}
Inside of app/views/announcements/_announcements.html.erb
we add a button that we can click to close the drawer:
<%= turbo_frame_tag :announcements do %>
<div class="fixed bg-white rounded max-w-lg p-4 right-3 top-3 bottom-3 shadow-md border overflow-y-scroll" data-controller="announcements">
<button data-action="announcements#close">close</button>
<%= render "announcements_list", announcements: @announcements %>
</div>
<% end %>
As you can see we added a button
that has a data-action
attribute - the default event for a button is click
so that’s why we write it as announcements#close
instead of click->announcements#close
- this makes it so when we click it, it invokes the close
function in the announcements
Stimulus controller.
If you open the drawer by clicking on the “What’s new” link, you’ll see the announcements being rendered on the right side of the page and if you click the “close” link, it’ll disappear. this.element
is the element that data-controller
is defined on, we can just call the DOM action remove()
on that element.
Animations
We want it to slide in when you click the link to make it a little more fun. Let’s go ahead and modify our tailwindcss.config.js
to add some custom animations. Add the following inside theme: { extend: {
theme: {
extend: {
keyframes: {
"slide-in": {
"0%": { transform: "translateX(100%)" },
"100%": { transform: "translateX(0)" },
},
"slide-out": {
"0%": { transform: "translateX(0)" },
"100%": { transform: "translateX(100%)" },
},
},
animation: {
"slide-in": "slide-in 0.3s ease-out",
"slide-out": "slide-out 0.3s ease-out",
},
},
},
Inside of app/views/announcements/_announcements.html.erb
we can now add the animate-slide-in
class to the div:
<%= turbo_frame_tag :announcements do %>
<div class="animate-slide-in fixed bg-white rounded max-w-lg p-4 right-3 top-3 bottom-3 shadow-md border overflow-y-scroll" data-controller="announcements">
<button data-action="announcements#close">close</button>
<%= render "announcements_list", announcements: @announcements %>
</div>
<% end %>
If you click on the “What’s new” link, you should see the drawer slide in (you might have to restart your server).
But if you click the close button, it just disappears. Let’s open up the stimulus controller app/javascript/controllers/announcements_controller.js
and change the close function to this:
close() {
this.element.classList.add("animate-slide-out")
setTimeout(() => this.element.remove(), 300)
}
As you can see we add the animate-slide-out
to the element and wait 300ms and then we remove the element. This is to ensure the animation has finished.
Close it when you click outside
We can easily add functionality where when you click outside of the drawer, we close it. Go ahead and add stimulus-use
, bin/importmap pin stimulus-use
and change your announcements_controller.js
to the following:
import { Controller } from "@hotwired/stimulus"
import { useClickOutside } from "stimulus-use"
export default class extends Controller {
connect() {
useClickOutside(this)
}
close() {
this.element.classList.add("animate-slide-out")
setTimeout(() => this.element.remove(), 300)
}
clickOutside() {
this.close()
}
}
Notice how we have imported useClickOutside
from stimulus-use
and are calling that inside the connect()
function which runs after the element is rendered on the page.
We then add the clickOutside
function, which is triggered when you click outside of the drawer.
And that is basically how you add a slide out drawer with what’s new in your product :)
You can of course style things differently, add links to take you to the specific announcement item etc.
Here’s a link to the example app: https://github.com/jespr/rails-whats-new-slide-out