#28: Moving to Elixir Part 4

Published December 13, 2017 7m 42s

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.

Ready to Learn More?

Subscribe to get access to all episodes and exclusive content.

Subscribe Now