Today we’re going to move our application from being completely unstyled to looking like something that people wouldn’t mind using. Since this tutorial series is about Rails we’ll learn how to interact with some additional front-end tools, but won’t necessarily cover all of the style details.
Goal for this Tutorial:
- Add Website Navigation
- Pull in Bootstrap Gem
- Style application
Styling our application won’t be the most difficult thing that we’ve done because we’re not here to learn more about CSS/SCSS or HTML. As of right now, the application is a little clunky to navigate around so we’ll start off by adding some navigation to allow a user to better move around the application. From there, we will be styling the main pages of our application. Although it’s a little cliche we will be using Twitter Bootstrap for our styles so that we don’t have to lay out a ton of CSS and we preemptively added some classes to our markup when we were writing it originally so that today’s work would be easier.
Installing Bootstrap
There are multiple ways to work with front-end dependencies in Rails, but the built in one is by using sprockets and front-end packages wrapped in gems. If you’re coming from a mostly front-end development background then this will feel really wrong, and I completely understand where you’re coming from, but we won’t be covering the use of bower, yeoman, yarn, webpack, etc.
The gem we will be using can be found here. Let’s add this to our Gemfile now.
Gemfile
gem 'bootstrap-sass', '~> 3.3.6'
We already have sass-rails
installed so we don’t need to worry about that portion of the README. After we rebuild our image and restart our application we’ll be set up to actually use Bootstrap in our application.
With the gem installed we can now load this into our main sass file. First, we’re going to switch from having an application.css
to having an application.scss
file so that we can use native sass features.
$ mv app/assets/stylesheets/application.css app/assets/stylesheets/application.scss
Next we can clean out this file and import Bootstrap:
@import "bootstrap-sprockets";
@import "bootstrap";
Lastly, we’ll set up our application.js
to load the additional Bootstrap JavaScripts:
//= require jquery
//= require bootstrap-sprockets
//= require jquery_ujs
//= require_tree .
If you refresh any page in the application now it will look slightly different based on the base styles provided by Bootstrap.
Adding Navigation
Before we get too involved with styling our individual pages we’re going to work on our application layout so that there will be some shared navigation and each page will have a similar frame. We want the user to be able to access the following if signed in:
- Recipes
- Meal Plans
- Log Out
If he/she isn’t logged in then the navigation should only show the “Sign In” option. Let’s tweak our app/views/layouts/application.html.erb
to make this happen.
app/views/layouts/application.html.erb
First we will modify some of the informational HTML tags, setting a language and a viewport:
<html lang="en">
<head>
<title>MealPlan</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
Next we’ll change out our header to be a bootstrap navigation item:
<header class="container">
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<%= link_to "Meal Planning", root_path, class: "navbar-brand" %>
</div>
<ul class="nav navbar-nav navbar-right">
<% if signed_in? %>
<li><%= link_to "Meal Plans", meal_plans_path %></li>
<li><%= link_to "Recipes", recipes_path %></li>
<li><%= link_to "Sign out", sign_out_path, method: :delete %></li>
<% else %>
<li><%= link_to "Sign In", sign_in_path %></li>
<% end %>
</ul>
</div>
</nav>
</header>
<section class="container">
<div id="flash">
<% flash.each do |key, value| %>
<div class="flash alert <%= alert_class(key) %>"><%= value %></div>
<% end %>
</div>
<%= yield %>
</section>
This will create a consistent layout and automatically style and of the flash messages that we create in our application, but we need to define the alert_class
method first.
_app/helpers/applicationhelper.rb
module ApplicationHelper
def alert_class(flash_type)
case flash_type.to_sym
when :notice
"alert-success"
when :alert
"alert-warning"
when :error
"alert-danger"
end
end
end
That added a lot more structure to our application and now we’re ready to tackle individual pages.
Styling Recipes
When I think of recipes I actually think on Pinterest. That’s where a lot of people find recipes and I think mimicking that representation for our recipe index would be great. Thankfully, Bootstrap provides a thumbnail component that will get us most of the way there (I’ll leave the variable height blocks up to you). Let’s change our recipes to lay them out in rows of 3/2/1 in small panels depending on screen size (using the thumbnail component).
app/views/recipes/index.html.erb
<div class="title-container">
<h1>Recipes</h1>
<%= link_to "New Recipe", new_recipe_path, class: "btn btn-default" %>
</div>
<div class="recipes">
<% @recipes.each do |recipe| %>
<div class="recipe">
<div class="thumbnail">
<div class="caption">
<h3><%= link_to truncate(recipe.name), recipe_path(recipe) %></h3>
<p><%= truncate(recipe.description, length: 150) %></p>
<p>
<%= link_to "Edit", edit_recipe_path(recipe), class: "btn btn-default" %>
<%= link_to "Delete", recipe_path(recipe), data: {
confirm: "Are you sure you want to delete: #{recipe.name}?",
}, method: :delete %>
</p>
</div>
</div>
</div>
<% end %>
</div>
We did change up the markup a fair bit here, and we used the truncate
helper so that we can make our recipe boxes all of a specific size. For that we do need some additional CSS though that we’re going to add to our application.scss
. We’ll be using flexbox to help keep our layout sane and get our content to flow the way we expect it to.
app/assets/stylesheets/application.scss
.title-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h1 {
margin: 0;
}
}
.recipes {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.recipe {
width: 30%;
min-width: 300px;
}
The “new” and “edit” views don’t have much in them, but we’ll want to wrap our titles in the title-container
class that we created so that our application looks consistent.
app/views/recipes/new.html.erb
<div class="title-container">
<h1>New Recipe</h1>
</div>
<%= render partial: "form" %>
The first thing we’ll want to do is get it styled with all of the proper bootstrap form classes and then see what we can do from there. Mostly just adding form-control
classes and setting the submit button to look like a bootstrap button.
_app/views/recipes/form.html.erb
<%= render partial: "shared/errors" %>
<%= form_for @recipe do |form| %>
<div class="form-group">
<%= form.label :name %>
<%= form.text_field :name, class: "form-control" %>
</div>
<div class="form-group">
<%= form.label :description %>
<%= form.text_area :description, class: "form-control" %>
</div>
<div class="actions">
<%= form.submit class: "btn btn-primary" %>
</div>
<% end %>
If you submit the form now though you can see that our errors container isn’t being rendered with any flare and bootstrap gives us some styles to make that more apparent.
_app/views/shared/errors.html.erb
<% if @errors.present? %>
<div class="alert alert-danger errors">
<ul>
<% @errors.each do |error| %>
<li><%= error %></li>
<% end %>
</ul>
</div>
<% end %>
The display of a recipe isn’t actually too bad in the recipe show page so I’ll leave adjusting this styles up to the reader. You’ll probably just want to change how the controls are displayed.
Styling Meal Plans
We’ll tackle the “new” view for meal plans to start. This view is a bit more complicated than the edit is because we have a second form, but it won’t take us that long to tweak.
_app/views/mealplans/new.html.erb
<div class="title-container">
<h1>New Meal Plan</h1>
</div>
<div class="panel panel-default">
<div class="panel-body">
<%= form_for @meal_plan, url: new_meal_plan_path, method: "GET", html: { class: "form-inline" } do |form| %>
<div class="form-group">
<%= form.label :start_date %>
<%= form.text_field :start_date, type: "date", class: "form-control" %>
</div>
<div class="form-group">
<%= form.label :end_date %>
<%= form.text_field :end_date, type: "date", class: "form-control" %>
</div>
<%= form.submit "Generate Plan", class: "btn btn-default" %>
<% end %>
</div>
</div>
<%= render(partial: "form") %>
This changes our top form to be in a panel to make it visually separate and we also make it an inline form since it’s so small.
Next, we’ll tackle the actual form, which currently looks horrible because we used the wrong class originally on our form groupings.
app/views/meal_plans/_form.html.erb
<%= render partial: "shared/errors" %>
<%= form_for @meal_plan, html: { class: "form-horizontal" } do |form| %>
<%= form.hidden_field :start_date %>
<%= form.hidden_field :end_date %>
<div class="meals">
<%= form.fields_for :meals do |meal_fields| %>
<div class="meal">
<%= meal_fields.hidden_field :id %>
<%= meal_fields.hidden_field :date %>
<div class="form-group margin-vertical-none">
<div class="col-sm-11 col-sm-offset-1">
<p><%= localize(meal_fields.object.date) %></p>
</div>
</div>
<div class="form-group">
<%= meal_fields.label :recipe_id, class: "col-sm-1 control-label" %>
<div class="col-sm-11">
<%= meal_fields.select :recipe_id, current_user.recipe_options, {}, class: "form-control" %>
</div>
</div>
</div>
<% end %>
</div>
<div class="form-group">
<div class="col-sm-11 col-sm-offset-1">
<%= form.submit class: "btn btn-primary" %>
</div>
</div>
<% end %>
This form probably has the most changes of any of our pages to improve the layout of the items. We’re not quite finished. We need to create this new class to remove vertical margin.
app/assets/stylesheets/application.scss
.margin-vertical-none {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
Additionally, now is the time for us to improve how we display times. We called localize
on the date for each Meal, but what does that do? It uses the date format that is localized for the user’s locale. We set this up by modifying the confing/locales/en.yml
file and setting the date format.
config/locales/en.yml
en:
date:
formats:
default: "%m/%d/%Y"
This doesn’t quite deal with all of it though. We also want to change how dates are rendered into strings to handle the to_s
on MealPlan
. So we need to utilize the localize
method on any date we render in the views, and use I18n.localize
when we interpolate a Date into a String in our models. We’ll start with MealPlan
.
_app/models/mealplan.rb
def to_s
"#{I18n.localize(start_date)} - #{I18n.localize(end_date)}"
end
Now in our views when we use <%= @meal_plan %>
it will always render the date’s in the proper format. I’ll leave it up to you to click around the site and see what other areas you would need to change to display all of the dates properly.
Recap
We went through most of the pages in our application to apply some styles and make the application look better. These changes included utilizing some flex-box and Bootstrap styles. Additionally, we took a glimpse at some of the internationalization (I18n) features that we have at our disposal. There are still some more styles that need to be written and I’ll leave that up to you (or you can check the associations pull-request). Next week we will deploy this application to a server on Digital Ocean.