The first thing we are going to add on to our app is pagination to our index
page. Basically the gist is, when we display all of our ideas on the index
page, when we have 100 ideas on there, users have to scroll forever to get to the bottom of the page. Instead, we probably want to divide them into separate pages, or in other words, paginate them.
Luckily, rails provides a gem for this that makes this super easy. We're going to be using a gem called pagy
.
Go to the documentation page here. We're going to be following the instructions from here.
The first thing they tell us to do is to add this line in the Gemfile:
gem 'pagy', '~> 6.2'
Let's go ahead and add that into your Gemfile
, then run bundle install
in the terminal to install it and restart the server.
Now if we look at the documentation under "It's easy to use and customize", it looks like implementing this is pretty simple.
We need to add include Pagy::Backend
to application_controller.rb
and include Pagy::Frontend
to application_helper.rb
Basically, we can just add pagy
to the query and it will work perfectly. We can also tweak other preferences as mentioned in the documentatioin.
In our ideas_controller.rb
, let's modify our code in the index
method.
def index
@pagy, @ideas = pagy(Idea.all)
end
While we're at it, right now if you add an idea, it isn't displayed at the top. We should make it so that the newest ideas are displayed first. We can do this by ordering it by the time it was created at.
Inside the index
method, let's modify the code so that it looks like this:
def index
@pagy, @ideas = pagy(Idea.order("created_at DESC"))
end
Awesome, now let's go back into the documentation. It also tells us how to render the page links in the view:
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
Let's go ahead and implement this in our views. Go into index.html.erb
and add this into the very bottom of the page:
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
Let's hop back into our browser and go to the home page to see if this actually worked.
You might notice that nothing shows up. This is probably because you don't have 20 ideas in the database right now. You can either add 20 ideas or change the configuration by the following method:
Create a pagy.rb
file in config/initializers
. Add Pagy::DEFAULT[:items] = 5
to the file and restart the server.
Once you do that, it should be fully working. Let's refresh the page.
We see now that the pagination links are there at the bottom of the page, but they aren't centered and styled. Let's go ahead and style them from this link. Change your application.tailwind.css
to look like this:
@import "~tailwindcss/base";
/*
Add the following markup AFTER your import statements
Notice: this style contains only the rules for pagy-nav
*/
.pagy-nav.pagination {
@apply isolate inline-flex -space-x-px rounded-md shadow-sm;
}
.page.next a {
@apply relative inline-flex items-center rounded-r-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20;
}
.page.prev a {
@apply relative inline-flex items-center rounded-l-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20;
}
.page.next.disabled {
@apply relative inline-flex items-center rounded-r-md border border-gray-300 bg-slate-100 px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20;
}
.page.prev.disabled {
@apply relative inline-flex items-center rounded-l-md border border-gray-300 bg-slate-100 px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20;
}
.page a, .page.gap {
@apply bg-white border-gray-300 text-gray-500 hover:bg-gray-50 relative inline-flex items-center border px-4 py-2 text-sm font-medium focus:z-20;
}
.page.active {
@apply z-10 border-indigo-500 bg-indigo-50 text-indigo-600 relative inline-flex items-center border px-4 py-2 text-sm font-medium focus:z-20;
}
Add this mt-5
class to the wrapper div for pagy. The pagy section should look like this:
<div class="mt-5">
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
</div>
Refresh the page and you should see that the page links are now beautiful and centered too. Cool!
Whenever we create an idea, update an idea, or delete an idea, it is a good idea to notify the user that the operation was completed successfully. On the other hand, it is also a good idea to notify the user if the operation failed.
We can achieve this using flash messages.
In your ideas_controller.rb
, let's set up these flash messages for the create
, update
, and destroy
actions.
We can setup flash
messages by assigning a string
values to different keys like this:
class IdeasController <ApplicationController
...
def create
@idea = Idea.create(idea_params)
if @idea.valid?
flash[:success] = "Your idea has been posted!"
else
flash[:alert] = "Woops! Looks like there has been an error!"
end
redirect_to root_path
end
...
def update
@idea = Idea.find(params[:id])
if @idea.update(idea_params)
flash[:success] = "The idea has been updated!"
redirect_to root_path
else
flash[:alert] = "Woops! Looks like there has been an error!"
redirect_to edit_idea_path(params[:id])
end
end
def destroy
@idea = Idea.find(params[:id])
@idea.destroy
flash[:success] = "The idea was successfully deleted!"
redirect_to root_path
end
...
end
To display the flash messages, let's create a new partial file inside of our app/views/shared
directory called _flash_messages.html.erb
.
Inside of this file, copy and paste the following code:
<% flash.each do |message_type, message| %>
<div class="flash-message" data-controller="flash">
<div class="flash flex items-center p-4 mx-2 md:mx-0 my-2 rounded-lg <%= flash_wrapper_classes(message_type) %>" role="alert">
<div class="text-sm font-medium">
<%= message %>
</div>
<button data-action="click->flash#dismiss" type="button" class="ml-auto -mx-1.5 -my-1.5 rounded-lg inline-flex items-center justify-center h-8 w-8 <%= flash_button_classes(message_type) %>" aria-label="Close">
<span class="sr-only">Close</span>
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
</button>
</div>
</div>
<% end %>
Also, go ahead and create a flash_messages_helper.rb
file inside app\helpers
. The file should contain the following code:
module FlashMessagesHelper
def flash_wrapper_classes(type)
if %w[alert danger].include?(type)
'text-red-800 rounded-lg bg-red-50'
elsif type == 'success'
'text-green-800 rounded-lg bg-green-50'
elsif type == 'warning'
'text-yellow-800 rounded-lg bg-yellow-50'
else
'text-blue-800 rounded-lg bg-blue-50'
end
end
def flash_button_classes(type)
if %w[alert danger].include?(type)
'bg-red-50 text-red-500 focus:ring-2 focus:ring-red-400 p-1.5 hover:bg-red-200'
elsif type == 'success'
'bg-green-50 text-green-500 focus:ring-2 focus:ring-green-400 p-1.5 hover:bg-green-200'
elsif type == 'warning'
'bg-yellow-50 text-yellow-500 focus:ring-2 focus:ring-yellow-400 p-1.5 hover:bg-yellow-200'
else
'bg-blue-50 text-blue-500 focus:ring-2 focus:ring-blue-400 p-1.5 hover:bg-blue-200'
end
end
end
Next, in your application.html.erb
add the _flash_messages.html.erb
partial above <%= yield >
:
<!-- HTML code above -->
<%= render 'shared/flash_messages' %>
<%= yield %>
<!-- HTML code below -->
Now if you create a new idea, update an idea, or delete an idea, your flash message should be displayed on to the screen!
You must notice that the flash message does not disappear at all, even when we click the close button. So, let's fix that first.
Go ahead and generate a flash controller in stimulus using the following command
rails g stimulus flash
Paste the following code into the flash controller file. The code enables the flash message to automatically disappear after 5000 ms i.e. 5 seconds. You can adjust the timing and code as required. Also, the dismiss method is triggered when user clicks on the cross button at the right end of the flash button. The method is responsible for removing the flash message.
import {Controller} from '@hotwired/stimulus';
// Connects to data-controller="flash"
export default class extends Controller {
connect() {
setTimeout(() => {
document.querySelectorAll('.flash').forEach((element) => {
element.outerHTML = '';
});
}, 5000);
}
dismiss(event) {
event.target.closest('.flash').outerHTML = '';
}
}