Dynamic enum fields in nested association forms in Rails Admin
We’ve had a few projects at Panoptic Development recently that made use of the RailsAdmin gem. Out of the box, RA satisfied about 95% of what we needed to do to complete each project, but there were a few edge requirements that we either had to make do without or find workarounds for. Don’t get me wrong, RA is a wonderful utility for quickly spinning up an administrative area for your Rails app. It’s loaded with really slick bells-and-whistles, and it has an incredibly flexible DSL for configuration. However, there are a few things it just doesn’t seem to be able to handle as-is and for which I’ve searched high and low for solutions for the last 9 months. I’m happy to report that I finally cracked one of these problems, and it wasn’t even all that complicated in the end.
The problem was this: For any given Changeset#model value, only a few values are valid for the Change#field attribute. Let’s start with the two models: Changeset and Change:
In RailsAdmin lingo, both of the Changeset#model and Change#field attributes are presented as enum fields. Here’s how this looks in RailsAdmin:
A nested “Changed Fields” fieldset inside a “Changeset” form
Out-of-the-box, you can provide RA with a list of possible values for any given enum field and it will create a <select> element with those values as options, then transform it into a combobox widget. When the enum field is part of a nested has_many association form, the <select> element and options are drawn once in a hidden “blueprint” <div> which is then copied for each new associated object you add. In other words, the enum values are evaluated once at runtime, and that’s that. What I needed was for these two enum fields to be linked, with the options available in the select lists of the children to be dependent on the selected value of the select list in the parent, and to do it dynamically.
The first step in solving this was to create a way that we can get a list of valid Change#field values for a given Changeset#model:
With that done it was then just a matter of reading through the RA source code, reverse engineering how it implements its filtered select widget and rebuilding the Change#field<select> elements with the new options:
updateAttributes accepts one argument, selects, which defaults to all of the nested Change#field fields in the form. We setup a couple events so that it will be called anytime either of the two enum fields’ value changes. When it is called it fires off an AJAX request to our new controller method and sets up a promise to to be run if the AJAX request completes successfully. When that happens it iterates over the array of valid field values that were returned and creates new <option> tags for each of them. Finally there’s a pretty long chain of function calls that: destroys the existing filteredSelect widget, finds and then removes all of the current <option> tags in the select except for the first one (it’s an empty option), appends the new <option> elements, creates a new filteredSelect widget (which picks up the new options), and sets the selected value to whatever was selected before.
The two change event triggers handle a couple different scenarios. The first case is if the Changeset#model field changes, in which case we update all existing nested Change#field<select> elements. The second case is when RA’s custom nested:fieldAdded event is triggered after a new nested association fieldset is generated (i.e. after the “Add a new Changed Field” button is pressed), and we update only that new Change#field field.
Perhaps this could be spun off into a new custom field type for RailsAdmin at some point. For now, I’m just happy it works for this particular project.