Rails Active Storage
On any web application, the ability to use images are tantamount. In a Ruby on Rails project, using Active Storage increases the flexibility to use external storage services and to seamlessly create user interaction.
In this article we will use Active Storage to allow a user to add an Avatar to their user profile. This Avatar will display on their Profile page and in the User Profile link in the Navbar.
TL;TR: The completed repository if you would like to jump straight to the code.
Setup
This is not a complete tutorial on the setup of our Rails project. Here are the basic features below. I do suggest you look at the repository.
- Basic Rails project with postgres
- A controller called
static
for the home page, and the root route specified asstatic#home
. - Add Bootstrap 4 using Webpacker
- Add Devise with the additional registration field of username added to the migration
- Use the gem
devise-bootstrapped
to preconfigure the Devise views with bootstrap styling.
Add the following Bootstrap navbar:
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-3">
<div class="container">
<%= link_to "Active Storage", root_path, class: "navbar-brand" %>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
<% if user_signed_in? %>
<li class="nav-item">
<%= link_to current_user.username, user_path(current_user.username), class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "Sign out", root_path, class: "nav-link" %>
</li>
<% else %>
<li class="nav-item active">
<%= link_to "Log in", root_path, class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "Sign up", root_path, class: "nav-link" %>
</li>
<% end %>
</ul>
</div>
</div>
</nav>
Create a profile page for the user:
<div class="d-flex align-items-center justify-content-center mt-5">
<div class="media mr-5 align-self-start">
Avatar
</div>
<div class="media">
<div class="media-body">
<div class="d-flex flex-row align-items-center justify-content-between">
<h1><%= @user.username %></h1>
<%= link_to "Edit", edit_user_registration_path, class: "ml-3 btn btn-secondary btn-sm" if current_user.id == @user.id %>
</div>
</div>
</div>
</div>
Add the route for the profile page: resources :users, only: [:show], param: :username, path: ""
Again, this is an overview of the setup, just look at this commit for the complete beginning source.
Active Storage
Active Storage gives us the option to use different storage services. We start by configuring the development environment, by adding the following to config/environments/development.rb
:
# Store files locally.
config.active_storage.service = :local
If you want to use Amazon S3 service in production, you add the following to config/environments/production.rb
:
# Store files on Amazon S3.
config.active_storage.service = :amazon
Refer to the documentation for more configuration options in production.
Remember Active Storage is part of Rails, so you just need to install to configure: rails active_storage:install
, then run the migration: rails db:migrate
.
Active Storage uses two tables in your application’s database named active_storage_blobs
and active_storage_attachments
. The active_storage_attachments
is a polymorphic join table that stores your model's class name.
In the Gemfile
, uncomment and bundle
install image_processing
. This gem allows us to resize our images. Make sure to restart your rails server.
Avatar
Add to the User model: has_one_attached :avatar
:
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_one_attached :avatar
end
We need to update the permitted parameter's method in the application controller for the avatar
:
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
private
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
devise_parameter_sanitizer.permit(:account_update, keys: %i[avatar name username])
end
end
Edit Profile
First, let's update the 'Edit Profile' form, so we can select an Avatar. You will find the template in app/views/devise/registrations/edit.html.erb
.
We need to add name
, and username
to the edit form:
<div class="form-group">
<%= f.label :username %>
<%= f.text_field :username, autofocus: true %>
</div>
<div class="form-group">
<%= f.text_field :name, autofocus: true, placeholder: "Name" %>
</div>
We need to add a place to display the Avatar, and a form picker to select the file. This will be placed at the top of the file. Take a look at the complete source for this file:
<div class="row">
<div class="col-sm-2">
<% if current_user.avatar.nil? %>
<%= image_tag f.object.avatar.variant(resize: "128x128!"), class: "rounded-circle m-4" %>
<% end %>
</div>
<div class="col-sm-10">
<div class="form-group">
<%= f.label :avatar %>
<%= f.file_field :avatar %>
</div>
</div>
</div>
We are testing if there is an Avatar that is associated with the current user, then displaying the image by using an image_tag
, resized to 128px, a feature of the image_processing
gem. We are using the Bootstrap classes to display the avatar as a rounded circle.
User Profile Page
So, we need to revisit our User show.html.erb
template file to include the newly selected avatar.
<div class="d-flex align-items-center justify-content-center mt-5">
<div class="media mr-5 align-self-start">
<% if current_user.avatar.nil? %>
<%= image_tag f.object.avatar.variant(resize: "128x128!"), class: "rounded-circle m-4" %>
<% end %>
</div>
<div class="media">
<div class="media-body">
<div class="d-flex flex-row align-items-center justify-content-between">
<h1><%= @user.username %></h1>
<%= link_to "Edit", edit_user_registration_path, class: "ml-3 btn btn-secondary btn-sm" if current_user.id == @user.id %>
</div>
</div>
</div>
</div>
This is the same code we used for the edit template. This is the result:
Navbar
Originally, I had placeholder paths in the Navbar, so we revisit to make the links workable. Also, we will include the avatar, and a check if one exist.
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-3">
<div class="container">
<%= link_to "Active Storage", root_path, class: "navbar-brand" %>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
<% if user_signed_in? %>
<li class="nav-item">
<%= link_to user_path(current_user.username), class: "nav-link" do %>
<% if current_user.avatar.nil? %>
<%= image_tag current_user.avatar.variant(resize: "24x24!"), class: "mr-1" %>
<% end %>
<%= current_user.username %>
<% end %>
</li>
<li class="nav-item">
<%= link_to "Sign out", destroy_user_session_path, method: :delete, class: "nav-link" %>
</li>
<% else %>
<li class="nav-item active">
<%= link_to "Log in", new_user_session_path, class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "Sign up", new_user_registration_path, class: "nav-link" %>
</li>
<% end %>
</ul>
</div>
</div>
</nav>
Not Perfect
So, this solution is not entirely practical. I do not like the approach where we are displaying a blank space if the avatar does not exist. In the next article we will build on this feature by using the users Gravatar as an optional or fallback image.
Footnote
This has been fun. Leave a comment or send me a DM on Twitter.
Shameless Plug: If you work at a great company and you are in the market for a Software Developer with a varied skill set and life experiences, send me a message on Twitter and check out my LinkedIn.