The to-partial-pathable duck type enables the rendering of otherwise non-renderable objects such as models. Models don’t respond to render_in, but they can be associated with a renderable partial through the to_partial_path method, which is why you can render them like this:
<%= render @article %>
I would like to propose we replace to_partial_path calls with more abstract to_renderable calls.
The default implementation of to_renderable on models could return arguments for render, delegating to the original to_partial_path methods, i.e.
def to_renderable
{ partial: to_partial_path }
end
Adding this layer of abstraction enables the use of other renderables such as ViewComponent and Phlex components.
My overriding to_renderable, for example, this Article model could declare it is to be rendered in the Views::Articles::Article component. A more abstract to_renderable on the ApplicationRecord could derive this component from the name of the model.
class Article < ApplicationRecord
def to_renderable
Views::Articles::Article.new(self)
end
end
I don’t know if there are any other renderable-object based view libraries besides ViewComponent and Phlex, but this change would make it easier to use any alternative view layer while maintaining ActionView defaults.
I’m happy to work on this, but I wanted to get some feedback on the idea before doing anything else.
That’s a great point. In which case, I think we should update calls to to_partial_path to instead pass the model as a renderable to render directly. render_in on models can then be defined as
def render_in(view_context, &block)
view_context.render(partial: to_partial_path, &block)
end
I haven’t tracked down all the call sites, but I know ActionText and some of the turbo streams stuff call to_partial_path directly.
Models can be anything that confirm the Active Model API, what you are proposing here is to expand the Active Model API to also require classes to implement an render_in method. I think this changes too much the API to push presentation concerns inside the Model layer. Note that before, to_partial_path would only return a string. With your proposal the model would be responsible for rendering itself, which isn’t a responsibility of the Model layer usually.
This same problem is also existent in the original proposal, but I’m more inclined to it since it has no knowledge about the view_context of the render methods of views, and it is only responsible to wrap itself in the presentation object.
I think the hooks should not be on the model layer but on the presentation layer.
The Article example can continue to return "articles/article" for the partial path, but ActionPack could call out to a view layer component that is registered with it to map that to the actual object that would do the rendering. After all "articles/article" is basically an (app-private) resource locator to locate the resource that can handle the rendering for an article. The mapping of that locator to the actual renderer is the responsibility of the presentation layer.
If Phlex is registered and responds with “Oh, I know how to render articles/article, use my Views::Articles::Article class to do the rendering”, then ActionPack can use that. If no custom view layer has a renderer registered for that partial path then ActionPack can use its current partial rendering mechanism.
I’m inclined to agree with Rafael as well as Joel’s original proposal. The mechanics of render_in feel like they belong squarely in the view layer. All that to_renderable would do is return a representational object related to the model, which the view layer can then render via render_in. What this component object does in particular, and how the view layer renders it, never becomes the direct concern of the model.
Where would you add that hook @Ufuk_Kayserilioglu? It should also be possible to have some resources rendered from ActionView while others are rendered from ViewComponent, etc. because migrating anything like this is a long process.
The one thing I don’t like about to_renderable in my original post is it doesn’t always return a renderable object. In the case of rendering a partial, it returns a hash of arguments that can be passed into the render method only because the render method accepts the partial: keyword argument as a Hash.
One option might be to have some kind of partial renderer object returned instead:
class PartialRenderer
def initialize(partial_path)
@partial_path = partial_path
end
def render_in(view_context, &)
view_context.render(partial: @partial_path, &)
end
end
I still think coupling rendering concerns to models isn’t the best way to model this. Looking at ObjectRenderer in the Rails codebase, I can see that the implementation could easily be extended to lookup how to render a given object by either looking up a table of partial paths to renderers (with the default being the default ActionView renderer), or by resolving through a callback to do the same. The callback method could, for example, resolve an appropriate renderer from the partial path by looking up the class name of a view component and falling back to the default ActionView renderer, if not found.
That would allow apps to configure how models will be rendered without telling the model what it will use for rendering. I think this or something like this would be the most flexible approach.