One of my commitments for this year is to learn more JavaScript, including: a) as a language, fundamentally, b) as a framework (i.e. VueJS), and c) as a part of Ruby on Rails.

I always knew I was good at juggling.

In regards to point (c), the upside of learning Ruby on Rails is that you don’t need a deep understanding of JavaScript (or even Ruby, for that matter) to get up and running. It’s entirely possible to spend months and months focused exclusively on practicing and writing good, clean Rails code. The downside, of course, is that you can end up with a knowledge of JS that doesn’t extend far beyond the basics.

And by you, I mean me.

Having decided that that needed to change, I wanted to start simple: deleting a Rails Active Record object using AJAX and vanilla JavaScript. In this particular case, my aim was to delete an instance of a Contact, which belongs to an Episode. I made it a particular point not to use jQuery, as everything jQuery does can be done with vanilla Javascript, and without the weight.

The Test

Because I’m not yet familiar with JavaScript testing, I relied on my current integration testing suite, which utilizes RSpec, Capybara, and Selenium.

# app/spec/features/managing_contacts_spec.rb

let! (:contact) { #code to create contact }

describe 'deleting a contact', js: true do
  before :each  do
    accept_confirm do
      click_on 'Delete contact'
    end
  end
  
  it 'flashes a successful message' do
    expect(page).to have_css alert_success
  end
  
  it 'does not display the contact' do
    expect(page).not_to have_content contact.body
  end
end

The View

The first order of business was changing the link that a user clicks to delete a contact. Adding remote: true is all it takes to enable Rails to make an AJAX request, rather than following the link in question:

<tr id="contact-<%= contact.id %>">
  <!-- shortened for brevity -->
  <td>
    <%= link_to "Delete contact", contact_path(contact), method: :delete, remote: true, data: { confirm: "Are you sure you want to delete this contact?" } %>
  </td>
</tr>

The Controller

Next, the controller must be able to handle the AJAX request and respond accordingly. At first, the destroy method served HTML responses, which is standard Rails. Changing it to respond with JS was simple with respond_to.

# app/controllers/contacts_controller.rb

def  destroy
  contact = Contact.find(params[:id])
  
  if contact.destroy
    respond_to do |format|
      format.js { flash.now[:success] = "Contact successfully deleted." }
    end
  end
end

The (vanilla) JavaScript

The script needed to do a number of things:

  • Select the right contact
  • Delete the selected contact
  • Display a flash informing the user that the contact has been deleted.

The last point is important to consider. ActionDispatch::Flash provides a convenient way of sending messages between server requests, but this AJAX remains client-sided. The following is what I came up with:

// app/views/contacts/destroy.js.erb

const container = document.querySelector(".container")

function removeRow(){
  const contactRow = document.querySelector("tr#contact-<%= contact.id %>")
  contactRow.parentNode.removeChild(contactRow)
}

function insertFlash(){
  const flash =  "<%= j render partial: 'shared/flash' %>"
  container.insertAdjacentHTML('afterbegin', flash)
}

removeRow();
insertFlash();

Let’s break it down.

First, because my contacts are displayed as rows in a table, selecting the right one meant selecting the right <tr> element with the appropriate #id. The row itself can be deleted in relation to its parent node.

Second, inserting the flash required escaping and rendering the right partial. Thankfully, the values in flash.now are immediately available within a request. This, however, yields text, and not HTML. Not to worry, insertAdjacentHTML() can convert text to HTML, and can do so as the first child of a particular node with the 'afterbegin' parameter.

A Wild Edge Case Appears!

Everything was working swimmingly and the integration test was passing. For good measure, though, I played around in the browser and found something curious. When I create a contact, a flash is displayed with a successful message. But what happens when I create a contact, and immediately try to delete it?

Two flashes are displayed. TWO! But of course. The initial flash persists until a new request is made, and the AJAX I implemented doesn’t make server requests. Thus, I needed a way to identify if a flash was already present, and, if so, remove/replace it. The end result was as follows:

// app/views/contacts/destroy.js.erb

function editFlash(){
  const oldFlash = document.querySelector("#flash")
  const newFlash = "<%= j render partial: 'shared/flash' %>"

  removeOldFlash(oldFlash)
  insertNewFlash(newFlash)
}

function removeOldFlash(flash){
  if (document.contains(flash)){
    container.removeChild(flash)
  }
}

function insertNewFlash(flash){
  container.insertAdjacentHTML('afterbegin', flash)
}

Covering the edge case only required checking if a flash was present, and then remove it if it did.

Another Test, Just in (edge) Case

Tests are almost necessary to ensure that efforts to refactor don’t break your code. Seeing as I’m a JavaScript beginner, I decided to add a little integration test to make sure a second flash didn’t appear.

Because the integration test, as whole, already creates a contact with a let! statement, and because I also needed a flash to “appear”, I used a separate context that would allow me to create a new contact in the test.

# app/spec/features/managing_contacts.rb

describe 'deleting a contact', js: true do
  context 'with a flash already present' do
    before :each do
      click_on 'Create contact'
      fill_in 'Body', with:  'This is a new contact.'
      click_on 'Create Contact'
      
      within("tr#contact-#{contact.id}") do
        accept_confirm do
          click_on 'Delete contact'
        end
      end
    end
    
    it 'flashes only one message at a time' do
      expect(page).to have_selector('#flash', count:  1)
    end
  end

  context 'regardless of flash presence' do
    # other tests
  end
end

With these tests, I feel like I’ve covered the very basics. At this point, I can rethink my test structure and wording, and can refactor code as necessary.

Conclusion

We walked through the process of modifying/creating the Rails views, controllers, and scripts necessary to delete an Active Record object.

At the end of the day, this wasn’t a bad foray into integrating custom JS into Rails. A number of CSS frameworks, including Bootstrap and Materialize, make use of jQuery snippets to add functionality to their components (drop-downs, navbars), but I’ll be looking to continue learning JS as it applies to the front-end. As it stands, it’s possible to do with vanilla JS what jQuery does, all without adding weight to a web application.

Special acknowledgments and thanks to Arun Kumar, a star moderator/contributor of The Odin Project, who pointed me in the right direction multiple times while I was attempting this.

Thanks for reading!