---
title: File Upload with Elm and ActiveStorage
teaser: Making Elm + Rails/ActiveStorage play nice together.
tags: activestorage,elm,rails
author: Anthony Moffa
published_on: 2020-03-20
---

File upload in Rails has gotten easier with the introduction of ActiveStorage.
The [official guide][activestorage-guide] is a great place to get started when
building a vanilla Rails app, but what about integrating ActiveStorage with an
Elm app consuming a Rails JSON API?

In this post we'll go over one way to accomplish this with examples from
[Plantiful][plantiful-repo], the [breakable toy][breakable-toy] I've been
working on since my thoughtbot apprenticeship. Plantiful is an Elm SPA backed by
a Rails API.

### Setting up ActiveStorage

On the Rails side of things, ActiveStorage setup is quite straightforward. Its
included in Rails 5 and 6. Your first step is to get the required tables into
the database:

	rails active_storage:install

This will generate a migration for you, `CreateActiveStorageTables`. This
creates a few tables to connect files to their associated records. So run that
migration:

	rails db:migrate

Excellent! Take a look at the [ActiveStorage Guide][activestorage-guide] to
learn how to configure ActiveStorage for different file-storage solutions.

Now to set up the model. This part is easy: it's just a single line. In
Plantiful, I started with a single image, an avatar for the plant. The model
looks something like this.

```ruby
class Plant < ApplicationRecord
  belongs_to :user
  has_many :check_ins

  has_one_attached :photo
end
```

That last line `has_one_attached :photo` is where the magic happens. This line
gives us some new methods we can call on a Plant instance. The method we’re
interested in right now is for attaching a photo, which we'll use in the
controller:

```ruby
class Api::PlantsController < Api::BaseController
  def photo
    @plant = current_user.plants.find(params[:id])
    @plant.photo.attach(plant_params[:photo])

    render json: serialized_plant, status: :ok
  end
  
  private
  
  def plant_params
    params.require(:plant).permit(:name, :photo)
  end

  def serialized_plant
    {
      id: @plant.id
      name: @plant.name,
      photo: url_for(@plant.photo)
    }
  end
end
```

And, of course, the route for that endpoint:

```ruby
namespace :api do
  resources :plants do
    post :photo, on: :member
  end
end
```

All the above steps should give us what we need on the back-end, so let's move
on to the front-end.

### File Upload with Elm

On the Elm side there are a couple of things we need to handle:

* Create an HTML tag on the page that triggers the file select dialog
* Create an HTTP request that sends the file to the server

To accomplish this, we need to handle a few different messages

* User clicks on file input
* User selects a file
* Elm client receives the server response

Let's start with the HTML tag. On the page where we want to add the photo upload
feature, there's an image tag pointing to a plants avatar or a placeholder
image. When the user clicks the image, we want the file select dialog to show.

We'll add a click handler to the image tag. Here's what that might look like:

```elm
Import File.Select as Select

type Msg
    = UserSelectedNewPhotoUpload
    | UserSelectedImage File.File

view : Model -> Html Msg
view model =
    div [] 
        [ img [ class "avatar"
              , src model.plant.photoUrl
              , onClick UserSelectedNewImageUpload
              ] []
        ]

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    UserSelectedNewImageUpload ->
      (model, Select.file ["image/*"] UserSelectedImage)

    UserSelectedImage _ ->
      (model, Cmd.none)
```

When the user clicks the photo, the `UserSelectedNewImageUpload` message fires
off. When we receive that message, we call `Select.file`.

`Select.file` needs two things: the type of file the user can select and a
message to send off once a file is selected. In our case, we’re accepting all
image types and we'll call our message `UserSelectedImage`. We need to handle
that new message, so let's update our `Msg` type and `update` function:

```elm
type Msg
    = UserSelectedNewPhotoUpload
    | UserSelectedImage File.File
    | ReceivedUploadResponse (Result Http.Error Plant)

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    UserSelectedNewPhotoUpload ->
      (model, Select.file ["image/*"] UserSelectedImage)

    UserSelectedImage image ->
      (model, uploadImage image model.plant.id ReceivedUploadResponse)

    ReceivedUploadResponse _ ->
      (model, Cmd.none)
```

Once a user selects an image, we'll receive the `UserSelectedImage` message
along with the image that was selected. We'll then pass those into `uploadPhoto`
to make HTTP request. Let's see what that looks like:

```elm
uploadPhoto :
    File.File
    -> Int
    -> (Result Http.Error Plant.Plant -> Msg)
    -> Cmd Msg
uploadPhoto image plantId msg =
    let
      url = "/api/plants/" ++ String.fromInt plantId ++ "/photo"
    in
    Http.request
      { method = "POST"
      , url = url
      , body = Http.multipartBody [ Http.filePart "plant[photo]" file ]
      , expect = Http.expectJson msg plantDecoder
      , headers = []
      , timeout = Nothing
      , tracker = Nothing
      }
```

So, in `uploadPhoto`, we’re making a POST request to the endpoint URL we defined
in our routes in the first part. For a plant with ID 2, we’re POSTing to
`/api/plants/2/photo`. Once we receive a response from the server, we'll fire
off the message we passed in, `ReceivedUploadResponse` and get the plant with
its updated `photoUrl`.

We'll also need a way to decode the JSON we receive back from the server. JSON
decoding is a deep topic in Elm and is outside the scope of this blog post. If
you're interested in diving into it, check out the [official guide][elm-json]
and this [great blog post by Joël][joel-decoders].

The `body` of the POST request is worth highlighting because it's a bit
different than the JSON encoded request body you'll often use in Elm. For our
use case with ActiveStorage, we don't want to encode request as JSON. Instead,
we want to send a request whose content-type is multipart/form-data. We want to
set the name of the `Http.filePart` in a way that matches up with Rails'
parameter conventions. If you're curious about how that works, take a look here:
[What Are Rails Parameters & How to Use Them Correctly][rails-params-article].
In our case, we're expecting parameters nested like so:

```ruby
irb(main):002:0> plant_params
=> { plant: { photo: photo_file_here } }
```

And that's exactly what `“plant[photo]”` gives us. When Rails decodes the
request, the photo key will have an `ActionDispatch::Http::UploadedFile` that
represents our image.

Great! So the final step here is handling `ReceivedUploadResponse`. We'll update
our `Msg` type and `update` function:

```elm
type Msg
    = UserSelectedNewPhotoUpload
    | UserSelectedImage
    | ReceivedUploadResponse (Result Http.Error Plant)

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    UserSelectedNewPhotoUpload ->
      (model, Select.file ["image/*"] UserSelectedImage)

    UserSelectedImage image ->
        (model, uploadImage image model.plant.id ReceivedUploadResponse)

    ReceivedUploadResponse (OK plant) ->
        ( { model | plant = plant }, Cmd.none

    ReceivedUploadResponse (Err _) ->
        (model, Cmd.none)
```

That should do it! Users can now upload beautiful images of their plant to
Plantiful. If you're curious to learn more about Elm, take a look at the [intro
guide][elm-guide]. For ActiveStorage, the [official docs][activestorage-guide]
cover everything you need to get started.

[activestorage-guide]: https://edgeguides.rubyonrails.org/active_storage_overview.html
[breakable-toy]: https://redsquirrel.com/dave/work/a2j/patterns/BreakableToys.html
[elm-guide]: https://guide.elm-lang.org/
[elm-json]: https://guide.elm-lang.org/effects/json.html
[plantiful-repo]: https://github.com/thestrabusiness/plantiful
[rails-params-article]: https://www.rubyguides.com/2019/06/rails-params/
[joel-decoders]: https://thoughtbot.com/blog/getting-unstuck-with-elm-json-decoders
