Rails Active Storage
In any web application, the ability to use images is tantamount. In a Ruby on Rails project, using Active Storage increases flexibility for integrating with external storage services and for seamlessly creating user interactions.
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.
TLTR: The completed repository, if you would like to jump straight to the code.
Setup
This is not a complete tutorial on setting up our Rails project. Here are the basic features below. I do suggest you look at the repository.
- Basic Rails project with PostgreSQL
- A controller called
staticfor 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-bootstrappedto 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; see this commit for the complete 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 the 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 need to install and 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 parameters 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 that 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_tagimage 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:
Originally, I had placeholder paths in the Navbar, so we revisit to make the links work. Also, we will include the avatar and check if one exists.
<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 that displays a blank space when the avatar does not exist. In the next article, we will build on this feature by using the user's Gravatar as an optional or fallback image.