# An ActiveStorage S3 Direct Uploader: Part 4 - Bonus Features

Our uploader is super functional and fast.

![Final Product](https://img.avi.nyc/X7nntB4c+)

## Quick Review

 Just for fun, I thought we'd add a feature where after the upload is complete we display an audio player for the track that was just uploaded. If we look back at our code, in our `Upload` class, we have the `process` method:

`app/javascript/controllers/uploadzone_controller.js`
 ```js
  process() {
  this.insertUpload();

  this.directUpload.create((error, blob) => {
    if (error) {
      // Handle the error
    } else {
      const trackData = { track: { filename: blob.filename }, signed_blob_id: blob.signed_id };

      post("/tracks", {
        body: trackData,
        contentType: "application/json",
        responseKind: "json",
      });
    }
  });
}
 ```

After the upload is sent to s3 via `this.directUpload.create`, in the callback for that we have our `post` call where we send the uploaded track information to our rails backend so that we can create the `Track` in our database and attach the file we uploaded to it. That `post` request, if you remember, returns a JSON representation of the `Track` created.

`app/controller/tracks_controller.rb`
```ruby
def create
  @track = Track.new(track_params)
  @track.audio_file.attach(params[:signed_blob_id])
  if @track.save
    render json: @track, status: :created
  end
end
```

The return for that looks like:

![Returning JSON](https://img.avi.nyc/6q9tDQrz+)

## Adding the Audio URL to JSON
[Commit](https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/b32c8d2e48ab44ed35ae566ddb8393704416aa56)

If we just include the URL of the track we just uploaded in that JSON, we could insert an `audio` tag into the DOM. Let's modify the JSON to include the URL for the Track. Let's update the `TracksController` to include a signed URL from S3 that will allow the Mp3 to be played:

`app/controller/tracks_controller.rb`
```ruby
def create
  @track = Track.new(track_params)
  @track.audio_file.attach(params[:signed_blob_id])
  if @track.save
    render json: {
      track: @track,
      audio_url: @track.audio_file.url
    }, status: :created
  end
end
```

Now the response from our `post` looks like this:

![Response](https://img.avi.nyc/h553Xs20+)

## Processing the JSON Response

With that we can grab the `audio_url` and replace the progress bar with an `audio` tag. Let's jump back to our `Upload` class in our `uploadzone_controller.js` and make that happen.

The first change is to make the callback to `this.directUpload.create` an `async` method so that we can tell it to `await` for the `post` request to complete. Then we need to capture the resulting JSON. For now, let's just log it to the console to make sure it works.

```js
  process() {
    this.insertUpload();

    this.directUpload.create(async (error, blob) => {
      if (error) {
        // Handle the error
      } else {
        const trackData = { track: { filename: blob.filename }, signed_blob_id: blob.signed_id };

        const trackResponse = await post("/tracks", {
          body: trackData,
          contentType: "application/json",
          responseKind: "json",
        });

        if (trackResponse.ok) {
          const trackJSON = await trackResponse.json;
          console.log(trackJSON);
        }
      }
    });
  }
```

Becaues this method is getting a bit long, let's make a mental note to refactor it, but the relevant addition we made is:

```js
const trackResponse = await post("/tracks", {
  body: trackData,
  contentType: "application/json",
  responseKind: "json",
});

if (trackResponse.ok) {
  const trackJSON = await trackResponse.json;
  console.log(trackJSON);
}
```

We're getting the response, making sure it was a 200 and it worked, then converting it to `json` and logging it. If we upload a track we'll see:

![Track JSON](https://img.avi.nyc/0LT85xz5+)

## Inserting the Audio Tag

With the audio_url, we have all the data we need to take the next step and replace the progress bar with an audio tag.

Let's make a new method for that, `insertAudio` and pass in the `audio_url`.

```js
insertAudio(audioURL) {
  // Find the progressBarDiv
  const progressBarDiv = document.querySelector(`#upload_${this.directUpload.id} .progress`);

}
```

The first thing we're doing is finding the progress bar div that we made in `insertUpload`. Once we have that div, we want to create an audio element, make some adjustments to the wrapper of the progress bar to make it taller, and then append the audio tag and remove the progress bar:

```js
insertAudio(audioURL) {
  const progressBarDiv = document.querySelector(`#upload_${this.directUpload.id} .progress`);

  // Create audio element
  const audio = document.createElement("audio");
  audio.controls = true;
  audio.src = audioURL;
  audio.classList.add("w-full");

  // Make the parent progressWrapper div taller
  progressBarDiv.parentElement.classList.add("h-14");
  progressBarDiv.parentElement.classList.remove("h-4");

  // Insert the audio tag and remove the progress bar.
  progressBarDiv.parentElement.appendChild(audio);
  progressBarDiv.remove();
}
```

With that we get this really awesome effect:

![Audio Track](https://img.avi.nyc/dfWMH7WC+)

A fully functional audio player after the upload.

## Refactoring
[Commit](https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/72d2fc0b223f9dcf68172d405a41262f377abe8b)

There are 3 code smells for me.

1. The `process` method is a bit long.
2. We're passing an argument into `insertAudio`.
3. We're finding a DOM element that we previously created.

Smells 2 and 3 are somewhat related, which is that whenever I'm in a pure object oriented paradigm, if I'm passing data that relates to the instance, like the audio_url, or finding data that I previously made again (like firing a SQL statement twice), I think to myself, hmm, shouldn't this be a part of the instance as a property?

Smell 1 is just that `process` seems to be doing 2 things, uploading the track and now processing json, that seems like a clear division. Let's do that refactor first and remove the logic for creating the track into its own method that we'll pass to the `this.directUpload.create` callback:

```js
process() {
 this.insertUpload();

 this.directUpload.create((error, blob) => this.createTrack(error, blob));
}

async createTrack(error, blob) {
 if (error) {
   // Handle the error
 } else {
   const trackData = { track: { filename: blob.filename }, signed_blob_id: blob.signed_id };

   const trackResponse = await post("/tracks", {
     body: trackData,
     contentType: "application/json",
     responseKind: "json",
   });

   if (trackResponse.ok) {
     const trackJSON = await trackResponse.json;

     this.insertAudio(trackJSON.audio_url);
   }
 }
}
```

Tada! Love a good extract method refactor.

Now the basic premise of passing arguments or refinding data in object orientation is make it a property. If we want to use `trackJSON.audio_url` in `insertAudio`, let's just make it a property of the instance of `Upload` instead of passing it around.

Instead of `const trackJSON = await trackResponse.json;` we'll do `this.trackJSON = await trackResponse.json;`

That's it. Once the `trackJSON` is part of the instance, inside `insertAudio` we can just read it when we need it. And we don't need to pass it when we call `insertAudio`.

```js
insertAudio() {
 const progressBarDiv = document.querySelector(`#upload_${this.directUpload.id} .progress`);

 // Create audio element
 const audio = document.createElement("audio");
 audio.controls = true;
 audio.src = this.trackJSON.audio_url;
 audio.classList.add("w-full");

 // Make the parent progressWrapper div taller
 progressBarDiv.parentElement.classList.add("h-14");
 progressBarDiv.parentElement.classList.remove("h-4");

 // Insert the audio tag and remove the progress bar.
 progressBarDiv.parentElement.appendChild(audio);
 progressBarDiv.remove();
}
```

No more argument and the important line is `audio.src = this.trackJSON.audioURL;`

The final code for `createTrack` looks like:

```js
async createTrack(error, blob) {
 if (error) {
   // Handle the error
 } else {
   const trackData = { track: { filename: blob.filename }, signed_blob_id: blob.signed_id };

   const trackResponse = await post("/tracks", {
     body: trackData,
     contentType: "application/json",
     responseKind: "json",
   });

   if (trackResponse.ok) {
     this.trackJSON = await trackResponse.json;

     this.insertAudio();
   }
 }
}
```

We're going to do a similar thing for the progress div. Once we create that element in `insertUpload`, we're just going to make a reference to that element in the DOM a property of the instance. We'll change `const progressBar = document.createElement("div");` to     this.progressBar = document.createElement("div");`.

Our `insertUpload` method now looks like this, with every reference to `progressBar` changed to be the property.

```js
insertUpload() {
  const fileUpload = document.createElement("div");

  fileUpload.id = `upload_${this.directUpload.id}`;
  fileUpload.className = "p-3 border-b";

  fileUpload.textContent = this.directUpload.file.name;

  const progressWrapper = document.createElement("div");
  progressWrapper.className = "relative h-4 overflow-hidden rounded-full bg-secondary w-[100%]";
  fileUpload.appendChild(progressWrapper);

  this.progressBar = document.createElement("div");
  this.progressBar.className = "progress h-full w-full flex-1 bg-primary";
  this.progressBar.style = "transform: translateX(-100%);";
  progressWrapper.appendChild(this.progressBar);

  const uploadList = document.querySelector("#uploads");
  uploadList.appendChild(fileUpload);
}
```

Now that the progressBar is a property of the instance, we can simplify our `insertAudio` yet again:

```js
insertAudio() {
  // Create audio element
  const audio = document.createElement("audio");
  audio.controls = true;
  audio.src = this.trackJSON.audio_url;
  audio.classList.add("w-full");

  // Make the parent progressWrapper div taller
  this.progressBar.parentElement.classList.add("h-14");
  this.progressBar.parentElement.classList.remove("h-4");

  // Insert the audio tag and remove the progress bar.
  this.progressBar.parentElement.appendChild(audio);
  this.progressBar.remove();
}
```


Everything still works! The final javascript controller looks like:

![Final Product](https://img.avi.nyc/nC0wQC4b+)

That's it for this series, post comments below and please follow me [@aviflombaum](https://twitter.com/aviflombaum).

