Rails Demo 4: Making things pretty with Bootstrap
Bootstrap is a popular front-end library released by Twitter that makes it easy to add professional styling to our web app. This demo continues the blog app you wrote previously.
The instructions for configuring Rails 6 to use Bootstrap were provided by this video: https://gorails.com/episodes/how-to-use-bootstrap-with-webpack-and-rails.
Step 1: First we need to install Bootstrap into our Rails app.
-
Use
yarn
to install thebootstrap
,jquery
, andpopper.js
packagesyarn add bootstrap jquery popper.js
-
Modify
config/webpack/environment.js
so that it looks like this:const { environment } = require('@rails/webpacker') const webpack = require("webpack"); environment.plugins.append("Provide", new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', Popper: ['popper.js', 'default'] })); module.exports = environment
The “
Provide
” plugin makes the$
,jQuery
, andPopper
names available to Bootstrap. - Create a file named
app/javascript/stylesheets/application.scss
that contains one line:@import "~bootstrap/scss/bootstrap"
- You will need to create the
app/javascript/stylesheets
directory. - Importing this
scss
file makes various bootstrap CSS variables available to JavaScript code. - Note: This
application.scss
file is different fromapp/assets/stylesheets/application.scss
.
- You will need to create the
-
Modify
app/javascript/packs/application.js
so that it looks like this:require("@rails/ujs").start() require("turbolinks").start() require("@rails/activestorage").start() require("channels") import "bootstrap" import "../stylesheets/application" document.addEventListener("turbolinks:load", () => { $('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="popover"]').popover(); });
import "bootstrap"
imports the Bootstrap-related JavaScript code.- The
addEventListener
reacts to a page load and runs the code necessary to make tooltips and popovers work.
-
Add the following line to
app/views/layouts/application.html
<%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
application.html.erb
is the template used by all views.- The erb code you write in your views is inserted by the
yield
. - The
stylesheet_pack_tag
causes every page to load the css packaged up by Webpacker.- This package contains the Bootstrap CSS
- Similarly, the
javascript_pack_tag
causes every page to load the JavaScript packaged up by Webpacker
Step 2: Show the various templates / components available with BootStrap:**
Step 3: Set up a Home page
-
Generate a
Home
controllerrails generate controller Home
-
Make sure the controller has an
index
method:class HomeController < ApplicationController def index end end
-
Add this route to
config/routes.rb
:root 'home#index'
-
Create the home page template:
views/home/index.html.erb
(You don’t need any content yet.)- Copy code from
basicColumns.html
to verify that the Bootstrap CSS is available and working. (Add some borders into thehome.scss
to make it easier to see what’s going on.)
- Copy code from
Step 4: Now Let’s add a Jumbotron to our homepage!
- Download a background image
cd
intoapp/assets/images
-
run
curl -L https://bit.ly/3bD7QJM --output bees.jpg
- The
-L
is important. - (The actual full URL for the bee image is https://raw.githubusercontent.com/jengelsma/CIS658-Winter18-BlogDemo/master/app/assets/images/bees.jpg)
-
Add the following to
views/home/index.html.erb
:<div class="jumbotron-fluid"> <div class="container"> <h1>Welcome to my world of bees!</h1> <p>The happiness of the bee and the dolphin is to exist. For man it is to know that and to wonder at it.</p> <p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more »</a></p> </div> </div>
-
Add the following to
home.scss
**.jumbotron-fluid { background-image: asset-url('bees.jpg'); background-size: cover; height: 400px; } .jumbotron-fluid h1 { color: #fff; text-align: center; margin-bottom: 30px; letter-spacing: -1px; font-weight: bold; } .jumbotron-fluid p { color: #fff; text-align: center; margin-bottom: 30px; font-weight: bold; }
- To see the bee image, you may have to
- Stop the server
- run
rake tmp:cache:clear
- start the server.
- run
- Stop the server
Step 5: Nice! Now let’s call out our most recent articles under the Jumbotron
-
Add the following to
index.html.erb
<div class="container-fluid"> <!-- Example row of columns --> <div class="row"> <div class="col-md-4"> <h2>Heading</h2> <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p> <p><a class="btn btn-default" href="#" role="button">View details »</a></p> </div> <div class="col-md-4"> <h2>Heading</h2> <p>Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui. </p> <p><a class="btn btn-default" href="#" role="button">View details »</a></p> </div> <div class="col-md-4"> <h2>Heading</h2> <p>Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p> <p><a class="btn btn-default" href="#" role="button">View details »</a></p> </div> </div> <hr> <footer> <p>© 2018 Company, Inc.</p> </footer> </div>
-
In your home controller, implement the index method to do the query.
def index @posts = Post.last(3) end
-
Now render these posts using a similar HTML structure to that from Step 1. Replace the hardwired Heading
div
s from with this:<% @posts.each do |post| %> <div class="col-md-4"> <h2><%= post.title%></h2> <p><%=post.article.truncate_words(30)%></p> <p> <%= link_to 'Read more »'.html_safe, post, class: "btn btn-default"%> </p> </div> <% end %>
Step 6: Now let’s add a standard nav bar to our app throughout.
-
Create a new partial named
views/layouts/_navigation.html.erb
and place this code in it<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <a class="navbar-brand" href="#">Bee Blog</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav mr-auto"> <li class="nav-item active"> <a class="nav-link navbar-dark" href="/">Home <span class="sr-only">(current)</span></a> </li> <li class="nav-item"> <a class="nav-link navbar-dark" href="/authors">Authors</a> </li> <li class="nav-item"> <a class="nav-link navbar-dark" href="/posts">Posts</a> </li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle navbar-dark" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Other Sites </a> <div class="dropdown-menu" aria-labelledby="navbarDropdown"> <a class="dropdown-item" href="https://www.gvsu.edu">GVSU</a> <a class="dropdown-item" href="https://www.cis.gvsu.edu">CIS</a> <div class="dropdown-divider"></div> <a class="dropdown-item" href="https://www.gatech.edu">Georgia Tech</a> </div> </li> </ul> <form class="form-inline my-2 my-lg-0"> <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search"> <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button> </form> </div> </nav>
-
Now invoke this partial in
<%= render 'layouts/navigation' %> <%= yield %>views/layouts/application.html.erb
:**
Step 7: Spruce up our Author pages
-
Modify
views/authors/index.html.erb
to look like this:<% if !notice.blank? %> <p id="notice" class="alert alert-success"><%= notice %></p> <% end %> <h1>Authors</h1> <table class="table table-hover"> <thead> <tr> <th class="w-25">First Name</th> <th class="w-25">Last Name</th> <th class="w-25">Email</th> <th class="w-25">Actions</th> </tr> </thead> <tbody> <% @authors.each do |author| %> <tr> <td class="w-25"><%= author.fname %></td> <td class="w-25"><%= author.lname %></td> <td class="w-25"><%= author.email %></td> <td class="w-25"> <%= link_to author, class: "btn btn-default btn-sm" do %> Show <% end %> <%= link_to edit_author_path(author), class: "btn btn-default btn-sm" do %> Edit <% end %> <%= link_to author, method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-danger btn-sm" do %> Delete <% end %> </td> </tr> <% end %> </tbody> </table> <br/> <%= link_to 'New Author', new_author_path, :class => "btn btn-primary" %>
-
Make the form partial for authors pretty as well. (Notice specifically that we’ve added bootstrap classes to the various elements.)
<%= form_with(model: author, local: true) do |form| %> <% if author.errors.any? %> <div id="error_explanation" class="alert alert-danger"> <strong><%= pluralize(author.errors.count, "error") %> prohibited this author from being saved:</strong> <ul> <% author.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <div class="form-group"> <%= form.label :fname , "First Name"%> <%= form.text_field :fname, id: :author_fname, class: "form-control" %> </div> <div class="form-group"> <%= form.label :lname, "Last Name" %> <%= form.text_field :lname, id: :author_lname, class: "form-control" %> </div> <div class="form-group"> <%= form.label :email, "Email" %> <%= form.text_field :email, id: :author_email, class: "form-control" %> </div> <div class="form-group"> <%= form.label :thumbnail, "Profile Image" %> <%= form.file_field :thumbnail, id: :author_thumbnail, class: "form-control-file" %> </div> <div class="actions"> <%= form.submit class: "btn btn-primary" %> <%= link_to 'Cancel', authors_path, class: "btn btn-danger" %> </div> <% end %>
-
Since we added a file upload type input, let’s doctor our controller a bit so it can extract the file name. Add these two lines of code at the top of the
create
andupdate
methods:thumb_upload = params[:author][:thumbnail] params[:author][:thumbnail] = thumb_upload.original_filename unless thumb_upload.nil?