In this article we will cover the integration between EmberJS and Active Storage, the new major feature released with Rails 5.2. We'll be introducing ember-active-storage, an addon we developed at Algonauti.
Introducing Active Storage
Active Storage facilitates uploading files to a cloud storage service (like Amazon S3 or Google Cloud Storage) and attaching those files to Active Record objects. One of its coolest features is direct uploads, which allows frontend to securely upload files directly to the cloud. This way, the backend app saves bandwidth, and the end users save time.
In the next sections, we'll be creating a demo project showing ember-active-storage
addon in action with a Rails 5.2 backend. It could take up to 20 minutes; if you're too busy, feel free to jump to the repository with the complete example code.
Create a new ember-cli-rails project
For a better understanding of what an ember-cli-rails
project is and its details, have a look at our dedicated article. Following is an updated list of commands using yarn as the frontend package manager.
$ rails new ember-active-storage-example --skip-bundle
$ bundle add ember-cli-rails
$ rails g ember:init
$ ember new frontend --skip-git --yarn
$ cd frontend
$ yarn add ember-cli-rails-addon --dev
In config/routes.rb
:
mount_ember_app :frontend, to: '/'
Active Storage Configuration
Active Storage uses two tables in your application’s database named active_storage_blobs
and active_storage_attachments
. Let's generate a migration that creates those tables, and update our database:
$ rails active_storage:install
$ bundle exec rake db:migrate
Generate a model with attachment
To our demo's purpose, let's create a Company
model with a logo
file attached.
$ rails g model company name:string
$ bundle exec rake db:migrate
Now, go to company.rb
and insert:
has_one_attached :logo
Let's move to the frontend folder and generate the ember model for Company
.
$ ember g model company name:string logo:string
Direct upload with ember-active-storage addon
Let's now move to the frontend folder and install the ember-active-storage
addon:
$ ember install ember-active-storage --yarn
We want to build a file-upload
component which will interact with the activeStorage
service provided by the above addon, in order to let our users upload files.
$ ember g component file-upload
We want the component to not be bound to a specific ember model, so we want to pass it an onFileUploaded
action which would implement model-specific logic with the uploaded file.
Let's start with the component's template: it consists of a file input and a very basic upload progress feedback:
<input
multiple='false'
onchange={{action 'upload'}}
accept='image/png,image/jpeg'
type='file'
/>
<p>Progress: {{uploadProgress}}%</p>
The upload
action will:
- send the selected file(s) to the
upload
method ofactiveStorage
service - listen to upload progress events
- on upload complete, trigger the received
onFileUploaded
action and pass it ablob
object
Let's see how it looks like:
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { get, set } from '@ember/object';
import { isPresent } from '@ember/utils';
export default Component.extend({
activeStorage: service(),
uploadProgress: 0,
actions: {
upload(event) {
const files = event.target.files;
if (isPresent(files)) {
const directUploadURL = '/rails/active_storage/direct_uploads';
for (var i = 0; i < files.length; i++) {
get(this, 'activeStorage')
.upload(files.item(i), directUploadURL, {
onProgress: (progress) => {
set(this, 'uploadProgress', progress);
},
})
.then((blob) => {
get(this, 'onFileUploaded')(blob);
});
}
}
},
},
});
A few more notes:
directUploadURL
is the path referencingActiveStorage::DirectUploadsController
on our Rails backend.- The
uploadProgress
property will hold a value between 0 and 100. - A
blob
object is anEmberObject
with useful properties describing the uploaded file; thesignedId
property is the most important one, because it allows Rails to link your ActiveRecord model to anActiveStorage::Attachment
. We'll see how in the next section.
Saving a model with attachment
Now that we have a file-upload
component, we want to use it for creating a company
record with a logo. We'll need some preparatory work:
- on the backend, basic APIs to create and list companies;
- on the frontend, basic pages to create a company and show all created companies.
Backend APIs
We'll be using active_model_serializers to generate JSON responses compliant to the JSON API standard - which is also the format Ember Data expects by default.
$ bundle add active_model_serializers
You'll also need an initializer for AMS, and make sure that :json_api
is set as the default adapter. See the AMS initializer on our example repo.
We can now generate a serializer for our Company model:
$ be rails g serializer company
Let's return the logo URL to the frontend, by using Active Storage features:
class CompanySerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :name, :logo
def logo
url_for(object.logo) if object.logo.attached?
end
end
Now, let's generate Company APIs:
$ rails g controller Companies index create
Implementation details are outside of the scope of this article, you can see the full controller on our example repo.
Company pages
First, let's generate a companies
route:
$ ember g route companies
Its model()
hook will return all companies by invoking findAll()
. See it on the example repo. The template will just loop over those companies and show name
and logo
for each. See it on the example repo.
Second, let's generate a companies.new
route:
$ ember g route 'companies/new'
Its model()
hook will return a new company
record. See it on the example repo. The template will show a form with a text input for name
and a file input for logo
; of course, we'll be using our file-upload
component. This is the focus of the current article, let's dive into it in the next section!
Create Company with logo
Here's how our create company template looks like:
<div class='container'>
<form>
<div class='form-group row'>
<label>Company Name</label>
{{input type='text' class='form-control' value=(mut (get model 'name'))}}
</div>
<div class='form-group row'>
{{file-upload
onFileUploaded=(action 'setCompanyLogo')
class='form-control-file'
}}
</div>
<div class='form-group'>
<div class='col-sm-offset-2 col-sm-10'>
<button {{action 'save'}} class='btn btn-sm btn-primary'>Save</button>
</div>
</div>
</form>
</div>
- The
setCompanyLogo
action will set the blob'ssignedId
into thelogo
attribute of thecompany
record. - The 'save' action will save the
company
record.
We need a controller to implement the above actions:
$ ember g controller 'companies/new'
Here's how it would look like:
import Controller from '@ember/controller';
import { get, set } from '@ember/object';
export default Controller.extend({
actions: {
setCompanyLogo(blob) {
set(get(this, 'model'), 'logo', get(blob, 'signedId'));
},
save() {
get(this, 'model')
.save()
.then(() => this.transitionToRoute('companies'));
},
},
});
What's more?
- We started with a pretty basic service in our
ember-active-storage
addon; we plan to extend it, and you are welcome to contribute :) - The benefits of a direct upload approach are more evident when your backend uses an external storage service. An article on configuring a Google Cloud Storage bucket and make it work with Active Storage is coming soon, stay tuned!
Powering your Ember apps with Rails' ActiveStorage via @algodave
Click to tweet