MojoForum - A Forum in Mojolicious (Part-3)

Posted by Ashutosh on May 16, 2020


Welcome back. This article is in continuation of learning the Mojolicious by developing a project from scratch. The name of the Series is MojoForum and, this is the second article of the series. For the previous write-ups, visit here.

The project repository on git is MojoForum.

In the Part-2, we accomplished:

  1. Feed the Database for User, Threads and Comments table
  2. Setup DBIX in Mojlicous
  3. Create thread controller
  4. Add styles to our Forums (Tailwind CSS)

Our forums looks good, however other important features are still missing from our application. Like the threads are not clickable and we are unable to view the comments.

Lets add some these features,

As of now our threads are not clickable. First thing we need to do is to convert out threads to anchor links. First thing that comes into the mind is how we want to access our threads. It should be /threads/id or ir should be threads/title_of_threads. The later one is SEO friendly where as the former one is not. For this tutorial we use the former approach and ignoring the SEO requirements :).

@threads = map { { 
  title => $_->title,
  body  => $_->body,
  user  => $_->user->first_name. ' ' . $_->user->last_name,
  id    => $_->id				# Add this id parameter also
} } @threads;

In previous article, where we create the thread controller, we need to add the id parameter also. I just realised I miss the date column for threads table in our previous article. We will add the date column in thread table in this article.

Before proceeding, We'll create the scaffolding of this route.

In the main file add the following route in the startup subroutine.

$r->get('/thread/:id')->to(controller => 'Reply', action => 'show');

Create the Reply controller under the Controller folder.

There are several ways of getting the Replies for a particular threads. One method is query the reply table and create an array out of it. And then store the title of thread in a variable. Just like below

my @replies = $dbh->resultset('Reply')->search({ thread_id => $thread_id });
    
@replies = map { { 
    user         => $_->user->first_name. ' '. $_->user->last_name,
    thread_id    => $_->thread_id,
    thread_title => $_->thread->title,
    body         => $_->body
} } @replies;

But there is one problem in the above approach, We already knew that these replies belongs to a particular thread id and we are repeatedly storing the thread_title in every loop. Here is another elegant approach to achieve this.

package mojoForum::Controller::Reply;
use Mojo::Base 'Mojolicious::Controller';

sub show {
    my $self = shift;

    my $dbh = $self->app->{dbh};
    my $thread_id = $self->stash('id');
    
    # Prefetch replies table where thread.id = replies.thread_id
    my @thread = $dbh->resultset('Thread')->search(
        { 'me.id' => $thread_id },
        { prefetch => 'replies'}
    );

    @thread = map { { 
        thread_id => $_->id,
        title     => $_->title,
        # Store all the replies of the thread as an array under replies
        replies   => [ map { {
                user => $_->user->first_name. ' '. $_->user->last_name,
                body => $_->body 
            } } $_->replies
        ],
        author    => $_->user->first_name. ' '. $_->user->last_name,
    } } @thread;

    my @replies = $dbh->resultset('Reply')->search({ thread_id => $thread_id });
    
    $self->stash(template => 'reply', thread => \@thread);
}

1;

By using the prefetch method, drastically reduce the number of queries to the database.

Code above looks good. But the only thing that stuck in mind is for getting the full name of the user we are writing

$_->user->first_name. ' '. $_->user->last_name

And we are using it on multiple places. How about a function that returns the full for us. We know that user is an object of User schema. So we'll create our custom function there.

Open the User.pm under Schema/Result

# Created by DBIx::Class::Schema::Loader v0.07049 @ 2020-04-28 21:43:28
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:SCr+ToZGYR0+s9ZnqCLjZQ

sub full_name {
    my $self = shift;
    return $self->first_name . ' ' . $self->last_name;
}

Go to the Thread controller and replace

# user  => $_->user->first_name. ' '. $_->user->last_name
user  => $_->user->full_name # replace the above line by this

Repeat the process for the Reply controller. And you are good to go with

Create the reply template now.

reply.html.ep under the templates folder.

% layout 'default';
% title 'MojoForum - A Forum with Mojolicious';

% foreach my $th (@$thread) {
    <div class="w-full border rounded py-3 mt-2">  

        <div class="text-blue-700 ml-5">
            
            <div class="flex">
                <p class="flex font-bold">Thread Title: 
                    <h2 class="ml-3"><%= $th->{title} %> </h2>  
                </p>
            </div>
            
            <div class="flex">
                <p class="flex font-bold">Posted By: 
                    <h2 class="ml-3"><%= $th->{author} %> </h2>  
                </p>
            </div>

        </div> 
        
    </div>

    <!-- Reply -->
    % foreach my $reply ( @{ $th->{replies} } ) {
        <div class="w-full border rounded py3 mt-2">
            
            <div class="flex">
                <p class="flex font-bold ml-3">Replied By: 
                    <h2 class="ml-3"><%= $reply->{user} %> </h2>  
                </p>
            </div>
               
            <div class="flex mt-3">    
                <p class="ml-3"><%= $reply->{body} %>  </p>
            </div>

        </div>
        
    %}
%}

So far we have setup the thread details router. When we visit localhost:3000/thread/1, we see the following page

But we haven't link it with the home page threads. Let's do that quickly.

Go to the thread.html.ep template

% layout 'default';
% title 'MojoForum - A Forum with Mojolicious';

% foreach my $thread (@$threads) {
    <div class="w-full border rounded py-3 mt-5">  
        
        <div class="flex font-bold text-blue-700 ml-5">
            <!-- Make the title clickable and change text over hover -->
            <a class="hover:text-blue-400" href="/thread/<%= $thread->{id} %>">
                <%= $thread->{title} %> 
            </a>
            
        </div> 
        
    </div>
    
%}

Refresh the home page and click on each threads will take to the thread details page.

There is an alternate way of creating anchor link in Mojolicious. The Mojo way of creating anchor link is much cleaner.

<a class="hover:text-blue-400" href="/thread/<%= $thread->{id} %>">
   <%= $thread->{title} %> 
</a>
<!-- Replace above text with -->
<%= link_to $thread->{title}, 'thread/'.$thread->{id} => (class => 'hover:text-blue-400' ) %>

Refresh the webpage.

Our forum looks much better now. But still there is lot of information on one page and we have to scroll it down to viewe all the threads. And it's not looking good to our eyes. Let's add the pagination.

We'll not use any third party pagination like Data::Page, just to keep things simple. And we also learn how we get the pagination using the Mojolicious framework only.

Lets start modify the main package i.e. mojoForum.pm and add these two sub routines. we re hard coding the things as of now, but we can set the pagination in the app config file.

sub _set_pagination {
    my $self = shift;
    $self->{paginate} = 10;
    return $self;
}

sub _get_pagination {
    my $self = shift;

    return $self->{paginate};
}

In the startup subroutine add the following:

$self->_db_handler();  ## Add after this line
$self->_set_pagination();

Now, we need to modify the Thread controller

package mojoForum::Controller::Thread;
use Mojo::Base 'Mojolicious::Controller';

# This action will render a template
sub show {
    my $self = shift;

    my $dbh = $self->app->{dbh};			    # Database handler
    my $paginate = $self->app->_get_pagination;	# paginate

		# IF page param passed from the Url
    my $page = ( !$self->param('page') ) ? 1 : $self->param('page');

    my $total_threads = $dbh->resultset('Thread')->search({})->count;

    # Fetch all the threads from the thread table;
    my @threads = $dbh->resultset('Thread')->search({}, 
        { rows => $paginate, page => $page }
    );
    
    @threads = map { { 
        title => $_->title,
        body  => $_->body,
        user  => $_->user->full_name,
        id    => $_->id
    } } @threads;
    
    $self->render(
    		# Template name, thread.html.ep under the templates folder.
        template     => 'thread',		
        # Pass thread array ref to the template
        threads      => \@threads,		
        # Total number of pages to be shown
        total_pages  => $total_threads / $paginate,
        # Current page
        current_page => $page
    );
}

1;

Now go to the thread template and replace the code with

% layout 'default';
% title 'MojoForum - A Forum with Mojolicious';

% my $prev_url = ($current_page == 1) ? '#' : '/?page='. ($current_page - 1);
% my $next_url = ($current_page == $total_pages) ? '#' : '/?page='.( $current_page + 1);

% foreach my $thread (@$threads) {
    <div class="w-full border rounded py-3 mt-5">

        <div class="flex font-bold text-blue-700 ml-5">

            <%= link_to $thread->{title}, 'thread/'.$thread->{id} => (class => 'hover:text-blue-400' ) %>

        </div>

    </div>

%}
<div class="flex p-4">
    <ul class="flex mx-auto list-reset border border-grey-light rounded">
        <li>
            <%= link_to 'Previous', $prev_url => 
                (class => 'block px-3 py-2 text-blue-700 hover:text-white hover:bg-indigo-500 border-r border-grey-light' ) 
            %>
        </li>
    % for my $page (1 .. $total_pages) {
        <li>
            <%= link_to $page, '/?page='. $page => 
                (class => 'block px-3 py-2 text-blue-700 hover:text-white hover:bg-indigo-500 border-r border-grey-light')
            %>
        </li>
    %}
        <li>
            <%= link_to 'Next', $next_url => 
                (class => 'block px-3 py-2 text-blue-700 hover:text-white hover:bg-indigo-500 border-r border-grey-light' ) 
            %>
        </li>
    </ul>
</div>

In the above, we iterate the the page from 1 to $total_pages and pagination is done.

Application works fine, however, there are couple of things left in pagination:

  1. Previous button is not disabled when we are at the home page or first page.
  2. Next button is not disabled when we at the last page.

We will check the current page and if the current_page is 1, then disable the Previous button and if the current page is last page to show thread, then disable Next. Now the template will look like

% layout 'default';
% title 'MojoForum - A Forum with Mojolicious';

% my $prev_url = ($current_page == 1) ? '#' : '/?page='. ($current_page - 1);
% my $next_url = ($current_page == $total_pages) ? '#' : '/?page='.( $current_page + 1);

<!-- Added to enable to disable the class -->
% my $prev_btn_class = _get_btn_class ( $current_page,  1 ) ;
% my $next_btn_class = _get_btn_class ( $current_page, $total_pages ) ; 

% sub _get_btn_class {
%   my ($current_page, $page_number )= @_ ;
%
%   my $btn = ( $current_page == $page_number ) 
%   ? 'block px-3 py-2 text-blue-700 hover:text-white hover:bg-indigo-500 border-r border-grey-light opacity-50 cursor-not-allowed'
%   : 'block px-3 py-2 text-blue-700 hover:text-white hover:bg-indigo-500 border-r border-grey-light' ;
% 
%   return $btn;
%}
<!-- Added upto here -->
% foreach my $thread (@$threads) {
    <div class="w-full border rounded py-3 mt-5">

        <div class="flex font-bold text-blue-700 ml-5">

            <%= link_to $thread->{title}, 'thread/'.$thread->{id} => (class => 'hover:text-blue-400' ) %>

        </div>

    </div>

%}
<div class="flex p-4">
    <ul class="flex mx-auto list-reset border border-grey-light rounded">
        <li>
            <%= link_to 'Previous', $prev_url => (class => $prev_btn_class ) %>
        </li>
    % for my $page (1 .. $total_pages) {
        <li>
            <%= link_to $page, '/?page='. $page => 
                (class => 'block px-3 py-2 text-blue-700 hover:text-white hover:bg-indigo-500 border-r border-grey-light')
            %>
        </li>
    %}
        <li>
            <%= link_to 'Next', $next_url => (class => $next_btn_class ) %>
        </li>
    </ul>
</div>

Go to the browser and visit the Home page, Previous button is disable now. Repeat the same process for the 5th page to disable the Next button.

Due to the Time constraint, I am unable to update the database with the timestamps. We will cover in our next tutorial.

In the next article, we'll cover:

  1. Add timestamps on Threads
  2. Add timestamps on comments.
  3. Create a new thread.
  4. Post a comment on threads.
  5. How to create and submit form using Mojolicious

The code has been updated to git repository. See you in the next tutorial.