Presenters to Widgets with ActiveModel::Conversion

Jon Rogers ·

Rails offers an easy way to render objects in a view that are automatically associated with their
own partials. With ActiveModel::Conversion, you can quickly build widgets out of classes (models or presenters or other).

The Problem

In a recent project, we had several little status/info blocks that we wanted to render on a page. Each block required different model data. In the controller, as we started writing code to fetch all the right data and put it together, we quickly realized that things were going to get ugly. We started with something like this:


You can see that as we added more items to put on the page, both the controller and view got bigger and more complex.

What we wanted was two-fold:

  1. Reduce the amount of work done by the controller to prepare data for the views
  2. Build an easy way to render the different chunks of data as separate widgets

The Solution

Presenters

The first step is to build some presenters. Following from the above example, we write two simple wrappers (presenters) to nicely prepare our data so it’s easy to access and render by a view.

At this point, we can simplify the way the controller fetches the data by leveraging these presenters

This solves the first issue, but it has not simplified the view. This is where ActiveModel::Conversion comes in.

Renderable Classes

By mixing ActiveModel::Conversion into our presenter classes, they suddenly know how to render themselves.

In both classes, we add:

include ActiveModel::Conversion

This adds the following methods: #to_model, #to_key, #to_param, and #to_partial_path to our classes. These methods are used by ActionController::Base#render. We can now render the object directly like so:

render @object

Rails is effectively doing this:

render @object.to_partial_path, :<model name> => @object

where to_partial_path generates a path that looks like <model names>/_<model name>

Now that we’ve added the mixin, we can update the view:

And to finish it up, we need to add view partials (whose path is going to match to_partial_path‘s response) for each of the presenter classes.


Now that we’ve got this far, we can do one more refactor:


At this point, the home page will render the two blocks, one showing hot stuff, and one showing recent activity. The partials are (for this example) pretty dumb, but you can imagine fleshing them out to pull from other methods that the presenters could offer.

Takeaways

  1. Using presenters is a simple way to compartmentalize logic that prepares model data for a view. That logic could be combining data from separate Rails models, or simple sorting, formatting or otherwise organizing the model data so that it is more easily accessible to the view in the way the view needs it.
  2. With ActiveModel::Conversion, the view can be directly tied to a presenter allowing it to act more like a widget that is easily rendered in your application views.

Sample Application – Widgetize Me!

Screenshot of Widgetize Me

Live on Heroku
| Source on Github

I wrote up a quick sample app that shows off this technique. The main page simply pulls a random set of widgets and renders them. The widgets include

  • SineWave
  • SquareWave
  • TriangleWave
  • RandomFunction
  • InstagramWidget
  • GithubWidget

and each one is basically a simple model and a view partial.

If you look through the app on github, you can see how things were built. The first four widgets require Javascript to draw the graphs. That’s all included in the partials for those models. The other widgets are wrappers on a thin feed puller and draw the first few (or random) entries from their respective feeds. The main controller, in this case, simply chooses a random array of these widgets and renders them. With each page refresh, you get a new set of widgets. Try it out!

tl;dr

Easily render a ruby class in Rails using ActiveModel::Conversion.

  1. Given a ruby class SuperWidget
  2. mixin ActiveModel::Conversion
  3. create the view partial in app/super_widgets/_super_widget.html.slim
  4. load it up in a controller @widget = SuperWidget.new
  5. render it in a view render @widget

References