#28: Moving to Elixir Part 4
Elixir 1.4.5
Phoenix 1.3
Kerosene 0.7.0
Number 0.5.4
Now that our URLs are updated and working, let’s work on some site improvements.
In this episode we’ll add pagination, ensure we’re only displaying cars that should be listed, and we’ll update our site to make it a little more pleasant to look at.
Let’s start with pagination. We’ll use the Kerosene package to handle our pagination.
To get started let’s open our “Mixfile”, and we’ll include it in our dependency list:
mix.exsdefp deps do
[
…
{:kerosene, "~> 0.7.0"},
…
]
end
Then we’ll download our new dependency from Hex.
$ mix deps.get
Now we’ll need to open our repo.ex
and configure Kerosene. We’ll include use Kerosene
and then include the per_page
option - this is what we want our default pagination to be. Let’s set ours to 8.
lib/dealership/repo.exdefmodule Dealership.Repo do
use Ecto.Repo, otp_app: :dealership
use Kerosene, per_page: 8
…
end
Then we’ll need to open the view we want to use for pagination - in this case car_view.ex
. And then we’ll import Kerosene.HTML
.
lib/dealership_web/views/car_view.exdefmodule DealershipWeb.CarView do
use DealershipWeb, :view
import Kerosene.HTML
end
Now we’ll need to paginate our cars. Let’s open our Listings
module. And Kerosene gives us the paginate
function that we’ll use to handle our pagination.
So let’s update this to:
/lib/dealership/listings/listings.ex def list_cars do
Repo.paginate(Car)
end
Now the paginate
function will return a two element tuple, so we’ll need to update our car_controller.ex
to handle it the change.
And in the index
function, we’ll update it to expect a response that’s the two element tuple.
The first element will be our list of cars, and the second element will be the pagination data that Kerosene will use in the template, so we’ll need to pass it in the assigns.
lib/dealership_web/controllers/car_controller.ex def index(conn, _params) do
{cars, kerosene} = Listings.list_cars()
render(conn, "index.html", cars: cars, kerosene: kerosene)
end
Now let’s head over to our car’s index.html.eex
template.
And we’ll include another paginate
function that will build our pagination links in our template. It takes our connection and the pagination data.
By default Kerosene will use Bootstrap styling, but it also has support for other CSS frameworks. Since we’re using Bootstrap the defaults will work.
And while we’re in this template, let’s remove some links we won’t need. The ‘new car’ link. Along with the ‘edit’ and ‘delete’ links. And let’s also remove the ‘listing cars’ text.
Template path: lib/dealership_web/templates/car/index.html.eex
…
<%= paginate @conn, @kerosene %>
With that we can go start our server again:
$ mix phx.server
And if we go to localhost:4000/cars
, we see only 8 cars displayed and pagination links have been included at the bottom of the page. While everything looks good visually, if we try to go to another page - we make a request to the server, but the same cars are being loaded.
This is because we’re not using the params - let’s fix that.
Back in our car_controller.ex
if we look at the index
function we’re ignoring the params with the underscore. Let’s change this to stop ignoring our params
and then let’s pass them into our Listings.list_cars
function.
lib/dealership_web/controllers/car_controller.ex...
def index(conn, params) do
{cars, kerosene} = Listings.list_cars(params)
render(conn, "index.html", cars: cars, kerosene: kerosene)
end
...
Then we’ll go to our listings.ex
module and update the list_cars
function to accept the params
. We’ll then pass them in as the second argument in Repo.paginate
.
lib/dealership/listings/listings.ex...
def list_cars(params) do
Repo.paginate(Car, params)
end
...
And if we go back to the browser and try the pagination links they work.
Now one thing we need to do is ensure that only cars that haven’t been sold are listed on our site. We’ll open our car.ex
module and there’s a field - car_isListed
- that we can use this to filter on.
Let’s create another function in our car module we can use to create our filter. Let’s call our new function for_sale
and because Ecto queries are composable, it will take a single parameter we’ll call query.
Then inside the function, we’ll use Ecto to query for all cars where the car_isListed
column is "Y".
While a more natural query would be where are cars listed is a boolean that’s true - we need to adapt to this legacy structure.
And since we’re using Ecto to query, let’s go to the top of our module and import Ecto.Query
.
Because we’re only using the ‘from’ function in Ecto.Query - we can specify that we only want to import that function with only
then the name of the function - from
- and its arity - 2
.
lib/dealership/listings/car.exdefmodule Dealership.Listings.Car do
use Ecto.Schema
import Ecto.Query, only: [from: 2]
...
def for_sale(query) do
from c in query,
where: c.car_isListed == "Y"
end
end
Then we’ll go back to our listings.ex
module and update it to use our new function:
def list_cars(params) do
Car
|> Car.for_sale()
|> Repo.paginate(params)
end
With this update we can rest easy knowing only cars listed for sale will be displayed.
Now lets make a few UI changes to our app. Let’s go to our app.html.eex
and let’s remove the phoenix logo and the ‘get started’ link. We’ll replace it with the name of our ‘Car App’. Let’s also include a link to the homepage.
If you can’t remember what route handles what page - we can go to the command line and use
$ mix phx.routes
Here we can see that our page_path
and :index
action handle our homepage. Let’s add the link:
template path: lib/dealership_web/templates/layout/app.html.eex
<header class="header">
<nav role="navigation">
<div class="row">
<div class="col-sm-6">
<h2>
Car App
</h2>
</div>
<ul class="nav nav-pills pull-right">
<li><%= link "home", to: page_path(@conn, :index) %></li>
</ul>
</div>
</nav>
</header>
Now let’s open our car’s show template. And here let’s update the page to display some of the important about our cars.
I’ll paste in the changes, but let’s walk through it. We’re using a heading for the year, make, and model of the car.
Then we’ve added a table to display the price, mileage, and description of a car. And we’re displaying the image for the car. Then at the bottom we’ve got a link back to our car’s index action.
Template path: lib/dealership_web/templates/car/show.html.eex
<div class="row">
<div class="col-sm-6">
<h3>
<%= "#{@car.car_Year} #{@car.car_Make} #{@car.car_Model}" %>_
</h3>
<table class="table table-striped">
<thead>
<tr>
<th></th>
<tr>
</thead>
<tbody>
<tr>
<td>
Price:
</td>
<td>
<%= @car.car_SellingPrice %>
</td>
</tr>
<tr>
<td>
Mileage:
</td>
<td>
<%= @car.car_Miles %>
</td>
</tr>
<tr>
<td>
Description:
</td>
<td>
<%= @car.car_Description %>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm-6">
<%= img_tag(@car.image_list, class: "img-thumbnail img-responsive") %>
</div>
</div>
<span>
<%= link "Back", to: car_path(@conn, :index) %>
</span>
Now looking at this, the car title we use here and on the ‘index’ template are the exact same.
Let’s create a simple helper function both templates can use to render the title.
Let’s open our car_view.ex
and create a new function car_title
that will take a car struct.
And it we’ll paste in the string we want to build.
lib/dealership_web/views/car_view.ex def car_title(car) do
"#{car.car_Year} #{car.car_Make} #{car.car_Model}"
end
Then let’s update our show.html.eex
template to use our new car_title
function.
Template path: lib/dealership_web/templates/car/show.html.eex
…
<h3>
<%= car_title(@car) %>
</h3>
…
And our index.html.eex
template to use the function as well.
Template path: lib/dealership_web/templates/car/index.html.eex
…
<td>
<%= car_title(car) %>
</td>
…
Now let’s open our app’s homepage and we’ll paste in some simple text and a link to our inventory.
Template path: lib/dealership_web/templates/page/index.html.eex
<div class="row marketing">
<div class="col-sm-6">
<h4>
Welcome to our car app!
</h4>
<p>
Browse the site and see if there's a car you like.
</p>
<p>
Find our inventory <%= link "here", to: car_path(@conn, :index) %>
</p>
</div>
</div>
Now let’s go back to our browser and check out our updated pages. Our homepage has our updates, and we can click to view our inventory. And our pagination is still working.
Now let’s check out a car - and perfect - all the information we want is being displayed. If we look at the details section of our car page - one thing isn’t quite right. The price and mileage aren’t formatted, which makes them a little more difficult to read at first glance.
To fix that let’s bring in the Number package, which will help us convert our numbers into different formats.
We’ll open our ‘Mixfile’ and add the package to our list of dependencies.
mix.exsdefp deps do
[
…
{:number, "~> 0.5.4"},
…
]
end
Then we’ll download our dependency:
$ mix deps.get
And restart our server:
$ mix phx.server
Now we can go to our car’s show.html.eex
template and let’s first format our numbers.
Instead of putting the logic in the template, let’s use helper functions to do the formatting. Let’s update the price to a function we’ll create named format_price
that will take the our car’s price. And another called format_mileage
that will take the cars raw mileage.
Template path: lib/dealership_web/templates/car/show.html.eex
…
<tr>
<td>
Price:
</td>
<td>
<%= format_price(@car.car_SellingPrice) %>
</td>
</tr>
<tr>
<td>
Mileage:
</td>
<td>
<%= format_mileage(@car.car_Miles) %>
</td>
</tr>
<tr>
<td>
Description:
</td>
<td>
<%= @car.car_Description %>
</td>
</tr>
…
Then let’s head to our car_view.ex
and implement both functions.
Let’s start with format_price
.
We’ll use the Number.Currency.number_to_currency
function to format the price.
Then for the format_mileage
function we’ll use the Number.Delimit.number_to_delimited()
to format it.
And since we don’t want to display any decimal places, we’ll set the ‘precision’ option to 0.
lib/dealership_web/views/car_view.exdefmodule DealershipWeb.CarView do
use DealershipWeb, :view
import Kerosene.HTML
def format_price(price) do
Number.Currency.number_to_currency(price)
end
def format_mileage(miles) do
Number.Delimit.number_to_delimited(miles, precision: 0)
end
def car_title(car) do
"#{car.car_Year} #{car.car_Make} #{car.car_Model}"
end
end
With that let’s go back to our site - and great our price and mileage is formatted.