Rails SPA with Active Admin and Active Storage
For my brother’s nano-brewery, I wanted to build a simple, single-page application in which he could create, display, update, and delete content via a database. He also needed to be able easily upload images. I found a few tutorials on building a Rails-backend API with React-frontend, but at the time of this article, I didn’t yet know much about React (or Angular for that matter) so we just went with Rails and Bootstrap.
For the content managememt I went with Active Admin, and Active Storage for images.
Building a simple Ruby on Rails single page app with Active Admin and Active Storage
Step 1. Generate a Ruby on Rails application, sans the boilerplate test directory (I prefer RSpec). Start the local server and verify it works on http://localhost:3000/
$ rails new SPA -T
$ cd SPA
$ rails s
Yay! We’re on Rails version 5.2.3, Ruby version 2.6.3
Step 2. Setup dependencies.
$ vim Gemfile
We’re going to do some alterations here. First, since we might want deploy this app to Heroku we’ll need to use PostgreSQL as our production database.
group :production do
gem 'pg'
end
In development and test we’re going to do the following…
- Move in
sqlite3
- Include
rspec-rails
A testing framework. - Include
factory_bot_rails
A library for setting up Ruby objects as test data. - Inlcude
shoulda-matchers
Simple one-liner tests for common Rails functionality - Include
faker
A library for generating fake data such as names, addresses, and phone numbers. - Include
database_cleaner
Can be used to ensure a clean state for testing.
group :development, :test do
gem 'sqlite3'
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'rspec-rails'
gem 'factory_bot_rails'
gem 'shoulda-matchers'
gem 'faker'
gem 'database_cleaner'
end
Lastly, setup dependencies for Active Admin and Active Storage.
- Include
activeadmin
The administration framework for Ruby on Rails applications. - Uncomment
mini_magick
A ruby wrapper for ImageMagick or GraphicsMagick command line.
# Active Admin
gem 'activeadmin'
gem 'devise'
#Use ActiveStorage variant
gem 'mini_magick', '~> 4.8'
The Gemfile should look something like this…
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '2.6.3'
gem 'rails', '~> 5.2.3'
gem 'puma', '~> 3.11'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.2'
gem 'turbolinks', '~> 5'
gem 'jbuilder', '~> 2.5'
# Active Admin
gem 'activeadmin'
gem 'devise'
#Use ActiveStorage variant
gem 'mini_magick', '~> 4.8'
gem 'bootsnap', '>= 1.1.0', require: false
group :development, :test do
gem 'sqlite3'
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'rspec-rails'
gem 'factory_bot_rails'
gem 'shoulda-matchers'
gem 'faker'
gem 'database_cleaner'
end
group :development do
gem 'web-console', '>= 3.3.0'
gem 'listen', '>= 3.0.5', '< 3.2'
# gem 'spring'
# gem 'spring-watcher-listen', '~> 2.0.0'
end
group :production do
gem 'pg'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
Install dependencies. You should see a lot of installation activity, culminating with a message similar to this.
$ bundle install
...
...
...
Bundle complete! 22 Gemfile dependencies, 100 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
Step 3. Cofigure the test suite.
Generate RSpec.
$ rails g rspec:install
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
Configure the test suite to use factory_bot_rails
- Create and edit a support file.
$ mkdir spec/support
$ touch spec/support/factory_bot.rb
$ vim spec/support/factory_bot.rb
- Include configuration.
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
- Require this file in
rails_helper.rb
require 'factory_bot'
Configure the test suite to use shoulda-matchers
- Place this code at the bottom of
rails_helper.rb
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end
Optional. Update .rspec
format.
$ vim .rspec
Mine looks like this…
--require spec_helper --format documentation
Run RSpec (making sure it’s not broken).
$ rspec
No examples found.
Finished in 0.00045 seconds (files took 0.14884 seconds to load)
0 examples, 0 failures
Step 4. Set our single page.
Generate the Controller. Notice the test specs being generated.
$ rails g controller page home
create app/controllers/page_controller.rb
route get 'page/home'
invoke erb
create app/views/page
create app/views/page/home.html.erb
invoke rspec
create spec/controllers/page_controller_spec.rb
create spec/views/page
create spec/views/page/home.html.erb_spec.rb
invoke helper
create app/helpers/page_helper.rb
invoke rspec
create spec/helpers/page_helper_spec.rb
invoke assets
invoke coffee
create app/assets/javascripts/page.coffee
invoke scss
Set the route
$ vim config/routes
Our config/routes
file should look something like this…
Rails.application.routes.draw do
root 'page#home'
end
Start the local server and verify it works on http://localhost:3000/
Go ahead an remove view and page_helper specs, I not going to use these for this application.
$ rm -rf spec/views
$ rm spec/helpers/page_helper_spec.rb
Since we have no database schema, now would be a good time migrate.
$ rails db:migrate
Verify tests are green.
$ rspec
Step 5. Install Active Admin.
$ rails g active_admin:install
invoke devise
generate devise:install
create config/initializers/devise.rb
create config/locales/devise.en.yml
===============================================================================
Some setup you must do manually if you haven't yet:
1. Ensure you have defined default url options in your environments files. Here
is an example of default_url_options appropriate for a development environment
in config/environments/development.rb:
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
In production, :host should be set to the actual host of your application.
2. Ensure you have defined root_url to *something* in your config/routes.rb.
For example:
root to: "home#index"
3. Ensure you have flash messages in app/views/layouts/application.html.erb.
For example:
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
4. You can copy Devise views (for customization) to your app by running:
rails g devise:views
===============================================================================
invoke active_record
create db/migrate/20190811205449_devise_create_admin_users.rb
create app/models/admin_user.rb
invoke rspec
create spec/models/admin_user_spec.rb
invoke factory_bot
create spec/factories/admin_users.rb
insert app/models/admin_user.rb
route devise_for :admin_users
gsub app/models/admin_user.rb
gsub config/routes.rb
append db/seeds.rb
create config/initializers/active_admin.rb
create app/admin
create app/admin/dashboard.rb
create app/admin/admin_users.rb
insert config/routes.rb
generate active_admin:assets
create app/assets/javascripts/active_admin.js
create app/assets/stylesheets/active_admin.scss
create db/migrate/20190811205453_create_active_admin_comments.rb
Migrate and seed the database.
$ rails db:migrate db:seed
== 20190811205449 DeviseCreateAdminUsers: migrating ===========================
-- create_table(:admin_users)
-> 0.0017s
-- add_index(:admin_users, :email, {:unique=>true})
-> 0.0008s
-- add_index(:admin_users, :reset_password_token, {:unique=>true})
-> 0.0008s
== 20190811205449 DeviseCreateAdminUsers: migrated (0.0039s) ==================
== 20190811205453 CreateActiveAdminComments: migrating ========================
-- create_table(:active_admin_comments)
-> 0.0030s
-- add_index(:active_admin_comments, [:namespace])
-> 0.0010s
== 20190811205453 CreateActiveAdminComments: migrated (0.0046s) ===============
Add an admin_user
spec.
$ vim spec/models/admin_user_spec.rb
Should look something like this…
require 'rails_helper'
RSpec.describe AdminUser, type: :model do
it { should validate_presence_of(:email) }
it { should validate_presence_of(:password) }
end
Verify tests are green.
$ rspec
Verify we can log in to our new Active Admin Content Management System.
- User: admin@example.com
- Password: password
$ rails s
Step 6. Generate Beer Model.
$ rails g model beer name:string style:string ibu:string abv:string description:text
invoke active_record
create db/migrate/20190813025408_create_beers.rb
create app/models/beer.rb
invoke rspec
create spec/models/beer_spec.rb
invoke factory_bot
create spec/factories/beers.rb
Create an Active Admin resource for Beer.
$ rails g active_admin:resource Beer
create app/admin/beers.rb
Write some model tests.
$ vim spec/models/beer_spec.rb
Should look something like this…
require 'rails_helper'
RSpec.describe Beer, type: :model do
it { should validate_presence_of(:name) }
it { should validate_presence_of(:style) }
it { should validate_presence_of(:ibu) }
it { should validate_presence_of(:abv) }
it { should validate_presence_of(:description) }
end
Migrate the database.
$ rails db:migrate
== 20190811205453 CreateActiveAdminComments: migrating ========================
-- create_table(:active_admin_comments)
-> 0.0030s
-- add_index(:active_admin_comments, [:namespace])
-> 0.0009s
== 20190811205453 CreateActiveAdminComments: migrated (0.0043s) ===============
== 20190813025408 CreateBeers: migrating ======================================
-- create_table(:beers)
-> 0.0009s
== 20190813025408 CreateBeers: migrated (0.0010s) =============================
Run tests…
$ rspec
Failed examples:
rspec ./spec/models/beer_spec.rb:4 # Beer should validate that :name cannot be empty/falsy
rspec ./spec/models/beer_spec.rb:5 # Beer should validate that :style cannot be empty/falsy
rspec ./spec/models/beer_spec.rb:6 # Beer should validate that :ibu cannot be empty/falsy
rspec ./spec/models/beer_spec.rb:7 # Beer should validate that :abv cannot be empty/falsy
rspec ./spec/models/beer_spec.rb:8 # Beer should validate that :description cannot be empty/falsy
Set Active Record validations in the Beer Model, and the ability to add images
$ vim app/models/beer.rb
Should look something like this…
class Beer < ApplicationRecord
validates :name, presence: true
validates :style, presence: true
validates :ibu, presence: true
validates :abv, presence: true
validates :description, presence: true
has_one_attached :image
end
Now tests are green.
Start the local server and verify we have the option to add beers http://localhost:3000/
$ rails s
Cool, let’s try it.
Whoops!
ActiveRecord::StatementInvalid in Admin::Beers#index
We need to update permitted parameters.
$ vim app/admin/beers.rb
Should look something like this…
ActiveAdmin.register Beer do
permit_params :name, :style, :abv, :ibu, :description, :image
form do |f|
f.semantic_errors # shows errors on :base
f.inputs do
f.input :name
f.input :style
f.input :ibu
f.input :abv
f.input :description
f.input :image, as: :file
f.actions
end
end
show do
attributes_table do
row :name
row :style
row :ibu
row :abv
row :description
row :image do |ad|
image_tag url_for(ad.image)
end
active_admin_comments
end
end
end
And we need to install Install Active Storage
$ rails active_storage:install
Copied migration 20190813042929_create_active_storage_tables.active_storage.rb from active_storage
Migrate the database.
$ rails db:migrate
== 20190813042929 CreateActiveStorageTables: migrating ========================
-- create_table(:active_storage_blobs)
-> 0.0021s
-- create_table(:active_storage_attachments)
-> 0.0020s
== 20190813042929 CreateActiveStorageTables: migrated (0.0046s) ===============
Setup the homepage skeleton.
$ vim app/views/page/home.html.erb
Something like this…
<h1>Page#home</h1>
<p>Find me in app/views/page/home.html.erb</p>
<h2>Here are some Beers!</h2>
<% @beers = Beer.all %>
<% @beers.each do |beer| %>
<h3><%= beer.name %> | <%= beer.style %></h3>
<p>ABV: <%= beer.abv %> IBU: <%= beer.ibu %></p>
<h4><%= beer.description %></h4>
<p><%= image_tag beer.image.variant(resize: "100x50") %></p>
<% end %>
Step 7. Start adding beers.