2. Adding Final Designs

Adding Pagination

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!

Adding flash messages

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 = '';
  }
}
Lesson list