Subscribe now

Phoenix and the Frontend [04.30.2017]

In the last episode, we saw how to get started using Phoenix to build a basic webapp, we looked at the various locations for generated files, and we talked through how things were wired together. Today we're going to have a look at the frontend in a bit more detail, adding categories and modifying the views to fit our needs. Let's get started.

Project

I've tagged the project we started yesterday in an orphaned branch in the firestorm repo. We'll rebuild the Ecto schema from before inside of this app to make it easier for people to learn Phoenix in a more basic setting.

We're going to create views for categories. We'll start by generating a resource:

mix phx.gen.html Forums Category categories title:string

We'll run the migrations:

mix ecto.migrate

And we'll update our router:

vim lib/firestorm_web/web/router.ex
defmodule FirestormWeb.Web.Router do
  # ...
  scope "/", FirestormWeb.Web do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    resources "/users", UserController
    resources "/categories", CategoryController
  end
end

Now we can manage categories. This is just another resource like users. Let's create some categories through the UI. We'll run the server:

mix phx.server

And create two categories:

  • Elixir
  • Elm

Now that we've created the categories, we see a list of them. We'd like this to look different. Specifically, we're ultimately wanting it to look like this design we generated as a result of the Firestorm Kickstarter along with the community. We'll ignore nested categories for now, but we'll start working towards this.

Let's open up lib/firestorm_web/web/templates/category/index.html.eex and make it match our anticipated HTML:

<span><%= link "New Category", to: category_path(@conn, :new) %></span>

<ol class="category-list">
  <%= for category <- @categories do %>
    <li>
      <h2 class="title">
        <%= link category.title, to: category_path(@conn, :show, category.id) %>
      </h2>
    </li>
  <% end %>
</ol>

Here we're just making an ordered list of categories, where each category has an h2 linking to the category using its title. This uses the link function from Phoenix.HTML. We also used a for comprehension to loop over our categories. We can extract the concept of linking to a category to a category_link function in our view, so that we always link to them the same way. Let's modify our HTML as if that function already existed:

<!-- ... -->
      <h2 class="title">
        <%= category_link(@conn, category) %>
      </h2>
<!-- ... -->

Now we'll generate a function in our CategoryView, which is available in our template:

vim lib/firestorm_web/web/views/category_view.ex

This is where you should put view-related functions that you want to reuse. These are loaded when rendering templates for their corresponding controllers. Let's add the function:

defmodule FirestormWeb.Web.CategoryView do
  use FirestormWeb.Web, :view

  def category_link(conn, category) do
    link category.title, to: category_path(conn, :show, category.id)
  end
end

Now it compiles, and we've got a function that helps us generate links to categories. This is just an example of how you can introduce helper functions to make your code less repetitive and make it easier to change later.

Let's add some CSS to help us style everything. My business partner, Adam Dill, helps me keep my CSS sane. We're introducing rscss and something like Inverted Triangle CSS for our CSS stack. These are just methodologies, not libraries, so we don't need to install anything - I just wanted to call them out.

New Phoenix projects use Brunch as their asset build tool by default, though you can swap it out. We'll stick with Brunch for the moment. I like to use sass to write CSS, so we'll install that:

cd assets
yarn add sass-brunch --dev

Now we can make *.scss files and they will be processed appropriately. Let's rename our app.css file to app.scss:

cd css
git mv app.css app.scss
// This should only have imports :)
//
// Inverted triangle for the import order, but modified a little bit for our
// purposes.
//
// Here's what Inverted Triangle CSS is:
// - https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/

// == SETTINGS ==
@import "settings.colors";
@import "settings.metrics";
@import "settings.global";

// == TOOLS ==
@import "tools.mixins";

// == ELEMENTS ==
// NOTE: This will hold element-specific styles in one file for// now, but I
// expect it to expand later
@import "elements";

// == PAGES ==
// @import "./pages/*";

// == LAYOUTS ==
// @import "./layouts/*";

// == COMPONENTS ==
// @import "./components/*";
// NOTE: We can't use "globbing" in sass with sass-brunch until the next release
// easily, so I'll list each component for now. I will ultimately switch to
// webpack so I didn't want to spend time fixing this for brunch.
@import "components/category-list";

There's a bunch of files we don't have yet. We'll create them rapidly:

touch _settings.colors.scss
touch _settings.metrics.scss
touch _settings.global.scss
touch _tools.mixins.scss
touch _elements.scss
mkdir components
mkdir pages
mkdir layouts
touch components/_category-list.scss

This is our general layout, though we haven't filled any of it out yet.

Let's start by defining our metrics. I'll paste in what we're using for the Firestorm design:

vim _settings.metrics.scss
// Settings - Metrics
// =================
// This holds numbers for things like default margins, paddings, border-radius,
// font-weights, etc.

$border-radius: .25rem;

$padding-small: .25rem;
$padding-medium: .5rem;

$margin-small: .25rem;
$margin-medium: .5rem;

$font-size-large: 1.5rem;
$font-size-medium: 1.25rem;

$font-weight-light: 300;
$font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-semibold: 600;
$font-weight-bold: 700;

Now we'll implement the category-list component, pasting these styles in again:

.category-list {
  padding: 0;
  margin-top: 0;

  .category-list {
    margin-left: 1rem;
  }

  > li {
    list-style-type: none;
  }

  > li > h2 {
    font-size: $font-size-large;
    font-weight: $font-weight-bold;
    margin: 0;
  }

  > li > .category-list {
    > li > h2 {
      // Children of the top-level category list need a smaller h2 that isn't
      // bold
      font-size: $font-size-medium;
      font-weight: $font-weight-normal;
    }
  }
}

This works, but it's not amazing because we haven't got any posts under the categories yet. Let's stay focused on the CSS. I'd like to build on top of PureCSS because it's a nice starting point. This also means I need to bring in a CSS reset - I'll use normalize. Finally, we'll add a typography library for our CSS, Typeplate.

yarn add --dev purecss normalize-css typeplate-starter-kit

Then we can reference these from our app.scss:

// ...
@import "tools.mixins";

// == GENERIC ==
@import "generic.normalize";
@import "generic.purecss";
@import "generic.typeplate";

@import "elements";
// ...

And make the individual files for each of them:

vim _generic.normalize.scss
// normalize.css - https://necolas.github.io/normalize.css/
@import "normalize";
vim _generic.pure.scss
// Pure.css - https://purecss.io/
@import "pure";
vim _generic.typeplate.scss
// Typeplate - http://typeplate.com/
@import "typeplate-index";

This might seem like a bit of work, but it helps us keep things clean. It won't work yet, because brunch doesn't know to look inside our node_modules packages for imports. We can configure brunch to allow this:

vim brunch-config.js
exports.config = {
  // ...
  // Configure your plugins
  plugins: {
    babel: {
      // Do not use ES6 compiler in vendor code
      ignore: [/vendor/]
    },
    sass: {
      options: {
        includePaths: [
          "node_modules/purecss/build",
          "node_modules/normalize-css",
          "node_modules/typeplate-starter-kit/dist/scss"
        ]
      }
    }
  },
  // ...
};

We'll also remove the default Phoenix CSS:

rm css/phoenix.css

Now if you look at the site, you'll see it's pretty bare bones. However, we have a CSS stack that we own now. Let's add our own colors. I'll paste in the Firestorm color swatch for now.

vim css/_settings.colors.scss
// Settings - Colors
// ===============
// What colors do we use in this app? They all go here. No exceptions. Don't go
// tinting things inline all willy-nilly.

// NOTE: Use this color naming tool to name our colors.
// http://chir.ag/projects/name-that-color/

$punch: #DB3440;
$yellow-orange: #FAAF3F;
$tower-gray: #acb7bf;
$silver: #BBBBBB;
$tundora: #444444;
$botticcelli: #dbe8f1;
$white: #ffffff;
$black: #000000;
$shadow-color: rgba($black, .24);
$iron: #d6dbdf;
$forest-green: #259B24;
$sky-blue: #64caec;

Now we can make our category headers be the primary red color, punch. But we don't want the category headers to be red for red's sake. We want them to be red because they are links. We'll define this CSS for the a element:

// Elements
// ===============
// What do HTML's generic elements look like?

a {
  color: $punch;
}

With that, we have styled the a element throughout our app to have the color we want.

Summary

Today we just generated a single resource and worked through styling it up a little, showing a bit of how Brunch works and how we can generate helper functions in our templates along the way. See you soon!

Resources