Tweeking Vanilla-Nested for Rails

Posted by Eli Cusic on May 26, 2021

The project I’m working on requires a nested form where a Deck has many Cards, and I wanted the user to be able to add and/or remove as many cards as they want. I was doing some research on how to accomplish this, and it seemed like finding a gem was the best way to go. I tried Cocoon, but I couldn’t get it to work for some reason. I found a different gem: vanilla_nested, which seemed like to be more streamlined, so I gave it a shot instead. I ran into a little bug with it, but I was able to get into the code, tweak it, and then it worked great for me, so I’m going to go through how I got it to work.

(GitHub link for vanilla_nested gem)

First of course I added gem vanilla_nested to my gemfile, and then since I’m using webpacker, in the CLI I input yarn add arielj/vanilla-nested. Last thing to do for the setup was to add import 'vanilla-nested' in my app/javascript/packs/application.js file. Then, it was a matter of setting up my nested form, which I made as a partial to be used in both the new and edit files in the deck views. Here is what my form looks like:

# app/views/decks/_deck_form.html.erb

<%= form_with model: [current_user, @deck], local: true do |f| %>
    <%= f.label :name, "Name: " %>
    <%= f.text_field :name %>

    <%= f.label :level, "Level: " %>
    <%= f.number_field :level, in: 1..8, step: 1 %>

    <% if current_user.admin %>
        <%= f.label :admin_approved, "Aprroved: " %>
        <%= f.check_box :admin_approved %>
    <% end %>

    <br><br>

    <h3><strong>CARDS:</strong></h3>
    <table>
        <thead>
            <tr>
                <td>English:</td>
                <td>Spanish:</td>
            </tr>
        </thead>

        <tbody id="nested-cards-table" class="fields">
            <%= f.fields_for :cards do |ff| %>
                <%= render 'card_fields', ff: ff %>
            <% end %>
        </tbody>
    </table>
    
    <br>

    <button>
        <%= link_to_add_nested(f, :cards, '#nested-cards-table', partial_form_variable: :ff) %>
    </button>

    <br><br>
    
    <%= f.submit "Submit Flash Cards" %>
    
<% end %>

And then here is what my cards fields partial looks like:

# app/views/decks/_card_fields.html.erb

<tr class="card-row">
    <td>
        <%= ff.text_field :english %>
    </td>
    <td>
        <%= ff.text_field :spanish %>
    </td>
    <td>
        <button>
            <%= link_to_remove_nested(ff, fields_wrapper_selector: 'card-row', link_text: "remove") %>
        </button>
    </td>
</tr>

For details on the syntax needed to set up the link_to_add_nested and link_to_remove_nested, I recommend visiting the GitHub link I included above, which gives a great breakdown of how to use the gem. This blog, however, is going to focus on a bug that took a bit of troubleshooting to get through. The issue was that since my link_to_remove_nested tag was not the direct child of its <tr class="card-row"> wrapper, which is the target that needs to be removed if clicked, and so when I clicked the link it would only remove the button. Face palm.

The fields_wrapper_selector: 'card-row' was supposed to take of this, but instead, I got this error in the console:

Uncaught TypeError: Cannot read property 'style' of null
    at hideWrapper (vanilla_nested.js:72)
    at HTMLAnchorElement../app/javascript/packs/vanilla_nested.js.window.removeVanillaNestedFields (vanilla_nested.js:62)

So, after returning to the vanilla_nested repo to figure out what to do, I ultimately decided to put the javascript file directly into my application. I created a vanilla_nested.js file with the code, and then added import './vanilla_nested.js' to my application.js file. Then, I started trouble shooting to see where the bug was. Ultimately, I found the issue in the block of code in lines 46-52. Here are those lines, with some console.logs mixed in:

let element = event.target;
if (!element.classList.contains('vanilla-nested-remove'))
  element = element.closest('.vanilla-nested-remove')
console.log(element)  // => <a> tag created by vanilla_nested gem
const data = element.dataset;
console.log(data)  // => DOMStringMap {fieldsWrapperSelector: "card-row", undoText: "Undo", undoLinkClasses: ""}
let wrapper = element.parentElement;
console.log(wrapper)  // => <button> element that the link is encased in
if (sel = data.fieldsWrapperSelector) wrapper = element.closest(sel);  
console.log(wrapper)  // => null

As you can probably see, something happens on that last line where the wrapper should be altered to the <tr class="card-row"> element, but instead produces null. After a bit of research and playing around with it, I realized that the element.closest( ) method needs a period preceding the class name (data.fildsWrapperSelector => “card-row”). So all it took was altering that line to this:

if (sel = data.fieldsWrapperSelector) wrapper = element.closest(`.${sel}`);

And there you have it. Just interpolate the sel variable behind the dot and then the element.closest( ) method works just fine.