Hooking up our polls index

We need to hook this amazing new logic up to our Vote controller's index function if we'd like to start taking advantage of it! We'll start it off simply enough and not worry about hooking up the paging portions of our code and verifying the end results. Our new index function in lib/vocial_web/controllers/poll_controller.ex should look something like this:


def index(conn, params) do
%{"page" => page, "per_page" => per_page} = Map.merge(%{"page" => 0, "per_page" => 25}, params)
polls = Votes.list_most_recent_polls(page, per_page)
render conn, "index.html", polls: polls
end

The first line in this controller (other than us changing the function arguments to actually care about params again) is the Map.merge statement. The reason behind this is that we want to be able to pull out the page and per_page arguments that a user may pass in, and handle them appropriately. This allows us to set up sane defaults for our page and per_page arguments and map them back out to the appropriate variable, which we then pass into the Votes.list_most_recent_polls/2 call.

Map.merge/2 takes in two arguments; the first is the map that we want to start with, and then the second is the map that will overwrite any duplicate information in the first. Given a base of %{ message: "Hello World", color: "red"} and a second map of %{ message: "Good morning!", age: 23 }, the resulting map would be %{ message: "Good morning!", color: "red", age: 23 }.

Other than that, the controller function remains identical! Let's take a look at the results (which if you ran your seeds file should actually look exactly like this):

But what if we want the paging stuff to actually function the way we're expecting it to? This is where things are going to start getting significantly more complicated! If we want this to be a bit more usable, we're going to have to make this logic significantly more complicated, I'm afraid!

The first issue we have to approach and deal with is how we're going to deal with pagination in general. First, we need to normalize out our page/per_page params before we can start doing anything with them since by default they're going to be typed as strings. We'll start off by building a helper function that will make sure the end result we pass along to our query will end up as integers when it gets to the database (still in lib/vocial_web/controllers/poll_controller.ex):

  defp paging_params(%{"page" => page, "per_page" => per_page}) do
page = case is_binary(page) do
true -> String.to_integer(page)
_ -> page
end
per_page = case is_binary(per_page) do
true -> String.to_integer(per_page)
_ -> per_page
end
%{"page" => page - 1, "per_page" => per_page}
end

We'll anticipate this working by just passing the params variable along to this function and getting a resulting map. If this page is is_binary (that is, if the page variable is storing a string), we'll want to convert it to an integer via String.to_integer/1. This will guarantee that our paging params are being passed to us in a format that we're expecting and can properly use. We'll create another helper function that will call out to paging_params, which will be responsible for normalizing the paging params entirely, including setting up default values for both the current page and the per_page size. One thing to note in the preceding function however, is that after we convert the page value to an integer, we decrease the value by 1. We do this so that the URL the user sees is page 1 for the user, instead of page 0 for the first page. Let's take a look at what our normalize_paging_params/1 function will look like:

  defp normalize_paging_params(params) do
%{"page" => 1, "per_page" => 25}
|> Map.merge(params)
|> paging_params()
end

What we're doing here is we're taking in the params passed to that page. We're including a default state of our params map that includes paging data, and then we're using that as the base to merge against params. This means that any values in the params map will be preserved, and if page/per_page do not exist, they will get values. Finally, the resulting map is sent off to the paging_params/1 function. This prevents us from needing multiple pattern match clauses for our function since we're normalizing everything first!

Next, we'll need to include some options to pass along to the template so that it knows what page we're on and whether it should include the previous/next page links on the page. Similar to the previous function's strategy, we'll use a default map and build values on top of that:

  defp paging_options(polls, page, per_page) do
%{
include_next_page: (Enum.count(polls) > per_page),
include_prev_page: (page > 0),
page: page,
per_page: per_page
}
end

At the top, we check the number of polls and see whether they're larger than the per_page value. The reason why we do this will be very clear later, but the short version is that we're actually going to request one more item than our per_page size. If we do this, using 25 as our example value, we'll request 26 items but limit what we actually get back to 25. This means that if we know there's at least one more object, there should also be at least one more page! This prevents us from doing extra count queries against our database and performing quick operations to build out pagination logic.

We'll set up the rest of our default arguments simply enough; by default, we determine whether the previous page link should be displayed by determining if we're on page 0 or not. Additionally, we include the page and per_page values to be passed along as the previous/next links.

Finally, let's take a look at our index function, which will actually be decently short because we already broke the logic out into a few separate helper functions:

  def index(conn, params) do
%{"page" => page, "per_page" => per_page} = normalize_paging_params(params)
polls = Votes.list_most_recent_polls(page, per_page)
opts = paging_options(polls, page, per_page)
render conn, "index.html", polls: Enum.take(polls, per_page), opts: opts
end

So, we start off by pattern matching out our page and per_page values from our call to normalize_paging_params/1. Next, we get the list of polls out of the database. Next, using the list of polls, we set up our options for displaying the page. Finally, we render the whole thing, now including our opts variable in addition to the polls. We also make sure that we only take the per_page amount for the polls. After all, we don't want to display 26 items if we say we're only including 25 items. If you go back and test out your UI, everything should mostly work, right?

Unfortunately not! If you test it out, you'll probably notice that as you're paging through, it's not actually giving us another page! The reason for this is that the logic in our context doesn't match what we're trying to accomplish exactly. What we need to do is modify our query in the context to include the extra item to make our pagination work correctly, but in a way that doesn't affect our limits or offsets. We'll write a new function for this in the context to keep our logic a little bit separate. In lib/vocial/votes/votes.ex, we'll create a new function:

  def list_most_recent_polls_with_extra(page \ 0, per_page \ 25) do
Repo.all(
from p in Poll,
limit: ^(per_page + 1),
offset: ^(page * per_page),
order_by: [desc: p.inserted_at]
)
|> Repo.preload([:options, :image, :vote_records, :messages])
end

Notice that the big difference here is that we're setting the limit to the per_page value plus one! We keep this function separate since we may want the original behavior, but since we only want to modify the per_page value once, we'll keep this functionality separate! We'll also write a quick test in test/vocial/votes/votes_test.exs to cover this behavior:

    test "list_most_recent_polls_with_extra/2 returns polls ordered and paged correctly", %{user: user} do
_poll = poll_fixture(%{user_id: user.id})
poll2 = poll_fixture(%{user_id: user.id})
poll3 = poll_fixture(%{user_id: user.id})
_poll4 = poll_fixture(%{user_id: user.id})
assert Votes.list_most_recent_polls_with_extra(1, 1) == [poll3, poll2]
end

Finally, we'll modify our controller (lib/vocial_web/controllers/poll_controller.ex) to use the new function instead:

  def index(conn, params) do
%{"page" => page, "per_page" => per_page} = normalize_paging_params(params)
polls = Votes.list_most_recent_polls_with_extra(page, per_page)
opts = paging_options(polls, page, per_page)
render conn, "index.html", polls: Enum.take(polls, per_page), opts: opts
end

Save it and we can move on to the final portion of this work, which is modifying the template. Open up lib/vocial_web/templates/poll/index.html.eex and add some pagination logic to the bottom:

<br/>
<div>
<%= if @opts.include_prev_page do %>
<div class="pull-left">
<%= link "<< Previous Page", to: poll_path(@conn, :index, [page: (@opts.page - 1), per_page: @opts.per_page]) %>
</div>
<% end %>

<%= if @opts.include_next_page do %>
<div class="pull-right">
<%= link "Next Page >>", to: poll_path(@conn, :index, [page: (@opts.page + 1), per_page: @opts.per_page]) %>
<div class="pull-right">
<% end %>
</div>

If we go back to our page and append the pagination params (I used page=2 and per_page=2), we should see a proper pagination, just like we expect! The end result should be a page that looks something like this:

That's a pretty good user experience! I think we can move on to some further optimization and polishing!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset