# Super Scaffolding with Delegated Types
## Introduction
In this guide, we’ll cover how to use Super Scaffolding to build views and controllers around models leveraging delegated types. As a prerequisite, you should read the [native Rails documentation for delegated types](https://edgeapi.rubyonrails.org/classes/ActiveRecord/DelegatedType.html). The examples in that documentation only deal with using delegated types at the Active Record level, but they lay a foundation that we won’t be repeating here.
## Terminology
For the purposes of our discussion here, and building on the Rails example, we’ll call their `Entry` model the **“Abstract Parent”** and the `Message` and `Comment` models the **“Concrete Children”**.
## One of Multiple Approaches
It’s worth noting there are at least two different approaches you can take for implementing views and controllers around models using delegated types:
1. Centralize views and controllers around the Abstract Parent (e.g. `Account::EntriesController`).
2. Create separate views and controllers for each Concrete Child (e.g. `Account::MessagesController`, `Account::CommentsController`, etc.)
**In this guide, we’ll be covering the first approach.** This might not seem like an obvious choice for the `Message` and `Comment` examples we’re drawing on from the Rails documentation (it's not), but it is a very natural fit for other common use cases like:
- “I’d like to add a field to this form and there are many kinds of fields.”
- “I’d like to add a section to this page and there are many kinds of sections.”
It’s not to say you can’t do it the other way described above, but this approach has specific benefits:
1. It’s a lot less code. We only have to use Super Scaffolding for the Abstract Parent. It's the only model with views and controllers generated. For the Concrete Children, the only files required are the models, tests, and migrations generated by `rails g model` and some locale Yaml files for each Concrete Child.
2. Controller permissions can be enforced the same way they always are, by checking the relationship between the Abstract Parent (e.g. `Entry`) and `Team`. All permissions are defined in `app/models/ability.rb` for `Entry` only, instead of each Concrete Child.
## Steps
### 1. Generate Rails Models
Drawing on the [canonical Rails example](https://edgeapi.rubyonrails.org/classes/ActiveRecord/DelegatedType.html), we begin by using Rails' native model generators:
```
rails g model Entry team:references entryable:references{polymorphic}:index
rails g model Message subject:string
rails g model Comment content:text
```
Note that in this specific approach we don't need a `team:references` on `Message` and `Comment`. That's because in this approach there are no controllers specific to `Message` and `Comment`, so all permissions are being enforced by checking the ownership of `Entry`. (That's not to say it would be wrong to add them for other reasons, we're just keeping it as simple as possible here.)
### 2. Super Scaffolding for `Entry`
```
rails generate super_scaffold Entry Team entryable_type:buttons --skip-migration-generation
```
We use `entryable_type:buttons` because we're going to allow people to choose which type of `Entry` they're creating with a list of buttons. This isn't the only option available to us, but it's the easiest to implement for now.
### 3. Defining Button Options
Super Scaffolding will have generated some initial button options for us already in `config/locales/en/entries.en.yml`. We'll want to update the attribute `name`, field `label` (which is shown on the form) and the available options to reflect the available Concrete Children like so:
```yaml
fields: &fields
entryable_type:
name: &entryable_type Entry Type
label: What type of entry would you like to create?
heading: *entryable_type
options:
"Message": Message
"Comment": Comment
```
We will add this block below in the next step on our `new.html.erb` page so you don't have to worry about it now, but with the options above in place, our buttons partial will now allow your users to select either a `Message` or a `Comment` before creating the `Entry` itself:
```erb
<% with_field_settings form: form do %>
<%= render 'shared/fields/buttons', method: :entryable_type, html_options: {autofocus: true} %>
<%# 🚅 super scaffolding will insert new fields above this line. %>
<% end %>
```
This will produce the following HTML:
```html
```
### 4. Add Our First Step to `new.html.erb`
By default, `app/views/account/entries/new.html.erb` has this reference to the shared `_form.html.erb`:
```erb
<%= render 'form', entry: @entry %>
```
However, in this workflow we actually need two steps:
1. Ask the user what type of `Entry` they're creating.
2. Show the user the `Entry` form with the appropriate fields for the type of entry they're creating.
The first of these two forms is actually not shared between `new.html.erb` and `edit.html.erb`, so we'll copy the contents of `_form.html.erb` into `new.html.erb` as a starting point, like so:
```erb
<% if @entry.entryable_type %>
<%= render 'form', entry: @entry %>
<% else %>
<%= form_with model: @entry, url: [:new, :account, @team, :entry], method: :get, local: true, class: 'form' do |form| %>
<%= render 'account/shared/forms/errors', form: form %>
<% with_field_settings form: form do %>
<%= render 'shared/fields/buttons', method: :entryable_type, html_options: {autofocus: true} %>
<% end %>
<% end %>
<% end %>
```
Here's a summary of the updates required when copying `_form.html.erb` into `new.html.erb`:
1. Add the `if @entry.entryable_type` branch logic, maintaining the existing reference to `_form.html.erb`.
2. Add `@` to the `entry` references throughout. `@entry` is an instance variable in this view, not passed in as a local.
3. Update the form submission `url` and `method` as seen above.
4. Remove the Super Scaffolding hooks. Any additional fields that we add to `Entry` would be on the actual `_form.html.erb`, not this step.
5. Simplify button logic because the form is always for a new object.
### 5. Update Locales
We need to add a locale entry for the "Next Step" button in `config/locales/en/entries.en.yml`. This goes under the `buttons: &buttons` entry that is already present, like so:
```yaml
buttons: &buttons
next: Next Step
```
Also, sadly, the original locale file wasn't expecting any buttons in `new.html.erb` directly, so we need to include buttons on the `new` page in the same file, below `form: *form`, like so:
```yaml
new:
# ...
form: *form
buttons: *buttons
```
### 6. Add Appropriate Validations in `entry.rb`
In `app/models/entry.rb`, we want to replace the default validation of `entryable_type` like so:
```ruby
ENTRYABLE_TYPES = I18n.t('entries.fields.entryable_type.options').keys.map(&:to_s)
validates :entryable_type, inclusion: {
in: ENTRYABLE_TYPES, allow_blank: false, message: I18n.t('errors.messages.empty')
}
```
This makes the locale file, where we define the options to present to the user, the single source of truth for what the valid options are.
TODO We should look into whether reflecting on the definition of the delegated types is possible.
Also, to make it easy to check the state of this validation, we'll add `entryable_type_valid?` as well:
```ruby
def entryable_type_valid?
ENTRYABLE_TYPES.include?(entryable_type)
end
```
I don't like this method. If you can think of a way to get rid of it or write it better, please let us know!
### 7. Accept Nested Attributes in `entry.rb` and `entries_controller.rb`
In preparation for the second step, we need to configure `Entry` to accept [nested attributes](https://edgeapi.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html). We do this in three parts:
In `app/models/entry.rb`, like so:
```ruby
accepts_nested_attributes_for :entryable
```
Also in `app/models/entry.rb`, [Rails will be expecting us](https://stackoverflow.com/questions/45295202/cannot-build-nested-polymorphic-associations-are-you-trying-to-build-a-polymor) to define the following method on the model:
```ruby
def build_entryable(params = {})
raise 'invalid entryable type' unless entryable_type_valid?
self.entryable = entryable_type.constantize.new(params)
end
```
Finally, in the [strong parameters](https://edgeguides.rubyonrails.org/action_controller_overview.html#strong-parameters) of `app/controllers/account/entries_controller.rb`, _directly below_ this line:
```ruby
# 🚅 super scaffolding will insert new arrays above this line.
```
And still within the `permit` parameters, add:
```ruby
entryable_attributes: [
:id,
# Message attributes:
:subject,
# Comment attributes:
:content,
],
```
(Eagle-eyed developers will note an edge case here where you would need to take additional steps if you had two Concrete Children classes that shared the same attribute name and you only wanted submitting form data for that attribute to be permissible for one of the classes. That situation should be exceedingly rare, and you can always write a little additional code here to deal with it.)
### 8. Populate `@entry.entryable` in `entries_controller.rb`
Before we can present the second step to users, we need to react to the user's input from the first step and initialize either a `Message` or `Comment` object and associate `@entry` with it. We do this in the `new` action of `app/controllers/account/entries_controller.rb` and we can also use the `build_entryable` method we created earlier for this purpose, like so:
```ruby
def new
if @entry.entryable_type_valid?
@entry.build_entryable
elsif params[:commit]
@entry.valid?
end
end
```
### 9. Add the Concrete Children Fields to the Second Step in `_form.html.erb`
Since we're now prompting for the entry type on the first step, we can remove the following from the second step in `app/views/account/entries/_form.html.erb`:
```erb
<%= render 'shared/fields/buttons', method: :entryable_type, html_options: {autofocus: true} %>
```
But we need to keep track of which entry type they selected, so we replace it with:
```erb
<%= form.hidden_field :entryable_type %>
```
Also, below that (and below the Super Scaffolding hook), we want to add the `Message` and `Comment` fields as [nested forms](https://guides.rubyonrails.org/form_helpers.html#nested-forms) like so:
```erb
<%= form.fields_for :entryable, entry.entryable do |entryable_form| %>
<%= entryable_form.hidden_field :id %>
<% with_field_settings form: entryable_form do %>
<% case entryable_form.object %>
<% when Message %>
<%= render 'shared/fields/text_field', method: :subject %>
<% when Comment %>
<%= render 'shared/fields/trix_editor', method: :content %>
<% end %>
<% end %>
<% end %>
```
We add this _below_ the Super Scaffolding hook because we want any additional fields being added to `Entry` directly to appear in the form _above_ the nested form fields.
### 10. Add Attributes of the Concrete Children to `show.html.erb`
Add the following in `app/views/account/entries/show.html.erb` under the Super Scaffolding hook shown in the example code below:
```erb
<%# 🚅 super scaffolding will insert new fields above this line. %>
<% with_attribute_settings object: @entry.entryable, strategy: :label do %>
<% case @entry.entryable %>
<% when Message %>
<%= render 'shared/attributes/text', attribute: :subject %>
<% when Comment %>
<%= render 'shared/attributes/html', attribute: :content %>
<% end %>
<% end %>
```
This will ensure the various different attributes of the Concrete Children are properly presented. However, the `label` strategy for these attribute partials depend on the locales for the individual Concrete Children being defined, so we need to create those files now, as well:
`config/locales/en/messages.en.yml`:
```yaml
en:
messages: &messages
fields:
subject:
_: &subject Subject
label: *subject
heading: *subject
account:
messages: *messages
activerecord:
attributes:
message:
subject: *subject
```
`config/locales/en/comments.en.yml`:
```yaml
en:
comments: &comments
fields:
content:
_: &content Content
label: *content
heading: *content
account:
comments: *comments
activerecord:
attributes:
comment:
content: *content
```
### 11. Actually Use Delegated Types?
So everything should now be working as expected, and here's the crazy thing: **We haven't even used the delegated types feature yet.** That was part of the beauty of delegated types when it was released in Rails 6.1: It was really just a formalization of an approach that folks had already been doing in Rails for years.
Really loving the PR for Rails 6.1's Delegated Types. From the application developer level, very little of it feels "new". Instead, the experience reads very similar to what many of us were already doing with the existing tools, but even smoother! https://t.co/6UkxXNCvaa
To now incorporate delegated types as put forward in the original documentation, we want to remove this line in `app/models/entry.rb`:
```ruby
belongs_to :entryable, polymorphic: true
```
And replace it with:
```ruby
delegated_type :entryable, types: %w[ Message Comment ]
```
We also want to follow the other steps seen there, such as defining an `Entryable` concern in `app/models/concerns/entryable.rb`, like so:
```ruby
module Entryable
extend ActiveSupport::Concern
included do
has_one :entry, as: :entryable, touch: true
end
end
```
And including the `Entryable` concern in both `app/models/message.rb` and `app/models/comment.rb` like so:
```ruby
include Entryable
```
## Conclusion
That's it! You're done! As mentioned at the beginning, this is only one of the ways to approach building views and controllers around your models with delegated types, but it's a common one, and for the situations where it is the right fit, it requires a lot less code and is a lot more [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) than scaffolding views and controllers around each individual delegated type class.