Are decorators still a thing?

I’ve noticed some presentation-related helpers migrating into my models lately and I’m wondering if things like Draper are still seeing much use out in the wild. Draper itself seems like it hasn’t seen a release since 2021. Otherwise how are folks handling presentation logic these days.

For added context the app I work on is mostly an api server these days, and mostly using jbuilder to kick out json responses.

I prefer to make POROs that represent everything that a view must render in a single object. This object can be either:

  1. passed into view, and you can call its methods in .erb/.jbuilder files
  2. itself be entirely ready for to_json
  3. all of the above. This is what I do. I don’t use jbuilder, instead I make an object that can be to_json, but also used in .erb files for html rendering.

I use portrayal for these objects (excuse the plug).

1 Like

As of I think Rails 5 or 6, you can use ActiveModel::Model to make active-record-like objects and I think this obviates the need for presenters/decorates, etc.

Suppose a User has one or more ContactPreference instances, each with a type like push notifications, email, text. Suppose you want to make a class to simplify this:

class UserContactPreferences
  include ActiveModel::Model

  attr_accessor :user, :sms, :email, :push

end

This class now behaves pretty much an Active Record, so you can do stuff like

<%= form_with model: user_contact_preferences do %>

and the like. Note that you may need to implement some methods depending on your routes, e.g. create vs. update:

class UserContactPreferences
  include ActiveModel::Model

  attr_accessor :user, :sms, :email, :push

  def to_param = user.to_param # used to create routes with ids in them

  def persisted? = true # if false, Rails will generate different routes 
                        # since it views this is a new record
end

How you create instances depends, but the simplest way is:

user_contact_preferences = UserContactPreferences.new(
  user: user,
  sms: user.contact_preferences.detect { |cp| cp.type == "sms" },
  email: user.contact_preferences.detect { |cp| cp.type == "email" },
  push: user.contact_preferences.detect { |cp| cp.type == "push" },
)

The use of attr_accessor provides that constructor.

If you are using a service layer, some service could do this, or you could put a self. method in the class itself:

class UserContactPreferences
  def self.from_user(user)

    # ...

  end

  # ...
end

This may seem a bit manual, but it has the virtue of using Rails stuff directly and not introducing a gem that may or may not get updated and may or may not work with new versions of Rails.

Also, to_json should just work.

[ Edit to add not on JSON + fix typo ]

1 Like

I would also add that portrayal is completely compatible with ActiveModel::Model, just adds a number of ruby object enhancements like support for dup, clone, hash equality, freeze, pattern matching all based on attributes.

And with basic ActiveModel there are a few quality of life things missing for forms, so I would normally do the following.

  1. Create an ApplicationStruct class that looks like this.
  2. Based on it, create an ApplicationForm that looks like this.

Here I describe what ApplicationForm does. Among other things, it helps secure parameters by auto-constructing Rails require/permit guards using attributes (auto-ignoring private attributes), and lets you do stuff like this in your controller:

case params
when MyForm; # do something
when MyOtherForm; # do something else
end

The project I linked above is a Rails template that leans on these concepts, and has READMEs to explain its philosophy.

While ViewComponents are very different than decorators I think they have overlapping purposes sometimes. Not as much if you are doing API servers but more if you are doing server-side rendering it might be a viable alternative to address some of the needs that Draper addresses. If you are not doing server-side rendering then agree that PORO are probably the way to go.

Slightly off topic, but why use e.g. ApplicationForm instead of ActiveModel?

class SomeResource
  include ActiveModel::Model

  attr_accessor :some_field, :some_other_field

  validates :some_field, presence: true
  validates :some_other_field, numericality: true

  def persisted? = false
end

# in a controller
def create
  @some_resource = SomeResource.new(
                     params.require(:some_resource).permit(
                                    :some_field, :some_other_field))

  if @some_resource.invalid?
    render :new
  else
    # some code needed here to do whatever
    # is supposed to happen with this resource
  end
end

# in ERB
<% if @some_resource.errors.any? %>
   Errors:
  <%= @some_resource.errors.full_messages.join(", ") %>
<% end %>
<% form_with model: @some_resource do |f| %>
  <%= f.text_field :some_field %>
  <%= f.numer_field :some_other_field %>

  <%= f.submit %>
<% end %>

# Also

some_resource.to_json #=> { some_field: value, some_other_value: other_value }

Seems like ActiveModel covers all the use cases identified in your link, but uses plain Rails stuff without having to learn anything new. It’s very close to what you get with a full on Active Record. You could even implement SomeResource.save and friends if you wanted it to look like one.

@davetron5000

Thank you for checking it out!

Main reasons for this pattern:

  1. These objects are already themselves param shapes, so no need to write params.require/permit manually.

    form = MyForm.from_params(params) # Auto-require/permit protection
    
  2. If you’d like to use these objects outside erb (e.g. serve as json), then they are missing some things that front-end needs: form’s action/method and “error/errors” pluralization. An ApplicationForm has that:

    // Our SignInForm
    {
      "phone": "",
      "password": "",
      "action": "/sign_in",
      "pluralError": "errors",
      "errors": {}
    }
    
  3. You get auto-exclusion of private attributes from both accepted params and to_json. They can be useful as allowlists for validations, or some other form config:

    validates :payment_method_id, inclusion: { in: :valid_payment_method_ids }
    
    private
    
    keyword :valid_payment_method_ids, default: []
    
  4. These objects inherit ApplicationStruct which is Portrayal-powered. This provides dup/clone support, freeze support, inheritance support (esp. for keyword defaults), and ability to set defaults based on instance-level procs instead of class-level procs. This would let you do stuff like (just a nonsensical example):

    keyword :employee_id, default: proc { rand(10000..99999) } 
    keyword :employee_email, default: proc { "#{employee_id}@example.com" }
    

If you look closely, it’s just a few sprinkles of convenience over PORO. I don’t want the base class to grow any bigger, and am actually thinking to remove my model_name change, probably not a good idea in retrospect. Had a bad reason for it.

I should probably explain the above better in the readme. Does that seem sensible?

Sorry, maybe it’s me, but I’m not following the code. They seem like alternative versions of things Rails already has (errors, pluralization, JSON). In my experience this makes things confusing for other team members because they aren’t sure if they should use the Rails-provided feature or the one from the presenter gem.

Oh, there is no presenter gem. I thought you looked at this link. Looks like you missed this in my earlier post. This is the entire ApplicationForm. 46 lines of code that does what you and I described above.

I’ll try again, shorter.

The ApplicationForm is not a gem. It’s a 46 line base class (that I published as part of my rails template) to help you construct form objects. No gems involved.

Well, almost. The Portrayal gem is involved, but it’s a plain ruby struct-class builder. Check out its readme linked in my first comment. It has absolutely nothing to do with any form or presenter logic, just a nice way to make POROs.

My entire recommendation is: make POROs with portrayal, but forms need a couple of extra things, so do write yourself a small base class for them, and include ActiveModel. That’s it.

They are still a thing and TBH it’s one of the topics worth standardizing in Rails. It seems to me that this has two main uses nowadays:

  1. Pure JSON serializer for use in the API, e.g. blueprinter
  2. View Object aka the actual “presenter”, for use in .erb templates, something that will contain methods like user_presenter.full_name (first and last name joined with ’ ') and various other variants of such methods that somehow modify the appearance of the model instance.

Of course, it would be great if both use cases could be handled by one high performance gem (i.e. blueprinter wouldn’t be necessary if the “presenter” implements convenient .to_json with different view options).

In my applications, I usually treat these 2 use cases separately, i.e. I use serializers for the API and for easier presentation of records in the UI I build dedicated “Presenter” classes, these can be objects based on ActiveModel or simpler ones based on SimpleDelegator

1 Like

I’ve implemented my own decorators inside a Tramway gem. It uses ViewComponent for rendering by default.

class UserDecorator < Tramway::BaseDecorator
  # delegates attributes to the decorated object
  delegate_attributes :email, :first_name, :last_name

  # you can provide your own methods with access to decorated object attributes with the method `object`
  def created_at
    I18n.l object.created_at
  end

  # you can provide representations with ViewComponent to avoid implementing views with Rails Helpers
  def posts_table
    render TableComponent.new(object.posts)
  end
end

Curious why you use serializers instead of to_json + Active Model?

In my experience, including deploying serveral API-only services built with Rails, Active Record and/or Active Models are all you need to create JSON payloads. to_json accepts sufficient options to either hide fields you don’t want or include dervied fields. When an API endpoint needs a vastly different shape/schema than what to_json would produce, we used Active Model to make a new PORO whose to_json produced the desired schema.

It worked really well and didn’t require any gems, base classes, or deviation from what is provided by Rails. Much of Rails’ behavior around this isn’t well documented, but it is stable and works well.

deploying serveral API-only services […] didn’t require any gems, base classes, or deviation from what is provided by Rails

For API-only, you don’t need a base class. Base class (specifically for forms) helps you reuse the same object in erb templates and API.

we used Active Model to make a new PORO whose to_json produced the desired schema

Portrayal is nice for this, because it lets you declare nested objects inline to make the whole json shape, while being compatible with ActiveModel.

class Company
  extend Portrayal

  keyword :name
  keyword :tax_id, default: nil

  keyword :address do
    keyword :street
    keyword :city
  end
end

@davetron5000 I guess using a gem for serializers just makes it easier + someone already took care of making it as performant as possible. See an example using blueprinter gem from procore:

class OrganizationBlueprint < Blueprinter::Base
  identifier :id

  fields :name, :display_name, :branding, :metadata

  association :projects, blueprint: ProjectBlueprint
end

that’s all you need to have a nicely formatted JSON responses including associations + gems usually give you extra features like different views depending on the user type (simple, extended etc.), so it’s just to avoid reinventing the wheel using more basic features from Rails.

In apps with large models, where you want to keep view-related code separated from other model functionality, I find the pattern very helpful, but I have not yet found a need to use draper or any gem for this. As @Janusz_M mentioned, Ruby’s standard library forwardable and SimpleDelegator work well in many situations (and you don’t have to worry about adding a dependency)

A basic one might look like this:

class UserPresenter < SimpleDelegator
  def welcome_message
    "Hello #{username}! Today is #{Time.now.strftime("%A")}."
  end

  def json_status
    { welcome_message: welcome_message,
      status: get_user_status }
  end
end