Better Rails Partial Rendering

Here was my problem. I wanted to render a polymorphic array like this.

E.g. templates named lions/lion or tigers/tiger as the case may be for each element in the array. As you probably guessed, my application didn’t actually use big cats, but a biological hierarchy is clear for the sake of our example.

This solution had been working perfectly, until I wanted to share these partials in another controller, under a Public namespace, causing the renderer to look in e.g. public/lions/lion rather than lions/lion for instance. I finally tracked down the problem to this method.

In searching for a partial, that Rails find_template method will do one of two things:

  1. If path has a / in it, then prefixes will be set to an empty array and it will search all the view directories for path. E.g. if path is lions/lion, then it will find a template like app/views/lions/_lion.
  2. Otherwise, it will traverse the controller hierarchy, e.g. if you have MeanLionsController < LionsController < ApplicationController, then a path of lion would look for a _lion partial in app/views/mean_lions/, then app/views/lions/, then finally app/views/application/.

In addition to this, the app/views/ directory isn’t the only one it’ll look for. There’s a list of view paths, and you can prepend or append to it with the controller class methods prepend_view_path and append_view_path. So the product of all these means there can be a considerable number of places to look.

to_partial_path

When passing in array of objects to be rendered, the path argument comes from the objects’ to_partial_path method, which is defined on ActiveModel::Base as essentially lions/lion (the plural then the singular model name). This means Rails doesn’t traverse the prefixes.

namespaces

In addition, if the controller is prefixed, then path also gets the prefix, e.g. lions/lion instead becomes public/lions/lion. This makes it impossible for our two controllers to reference the same partials in the default listing rendering case. We really need to prevent this namespacing. Rails thankfully gives us a setting to exclude this namespace prefixing on path in application.rb.

With this change, the path is now lions/lion instead of public/lions/lion. So we can simply put our template at app/views/lions/_lion and all is well.

One little problem

The thing is, what do we do when we want to use the namespaces, such as when the namespaced controller doesn’t need to share its partials? Why couldn’t we have app/views/public/lions/_lion for instance? For this use case, we could use prepend_view_path('app/views/public'). Whereas if two or more related controllers share a common special partial (related to zoos for instance), they could invoke prepend_view_path('app/views/zoo').

But if the polymorphic collection has both objects that need to render the standard app/views/lions/_lion and also the app/views/zoo/lions/_lion, then the only solution is to write a custom to_partial_path method, either on the model itself, or on a presenter object.

In that case, a path without a / could be convenient if those controllers share a common ancestor. Suppose all three controllers inherit from ZooController. Then a partial path such as lion would look under app/views/zoo/_lion in any of those controllers.

Un-DRY

If you really don’t care about DRY, you can ultimately have infinite flexibility via the custom to_partial_path method. By using presenters, you could even make different instances of the same model (e.g. different Lion records) render completely different partials deeply nested in unrelated locations under app/views. I recommend you avoid this unless absolutely necessary, but the flexibility is there if you need it.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Clay Shentrup

Clay Shentrup

advocate of score voting and approval voting. software engineer. father. husband. american.