PoC: AbstractController::Base#renderer_for#46202
Conversation
Currently you can customize the behavior of `render some_object` in
two ways:
- The object can respond to `to_partial_path` and return a view path.
- The object can respond to `render_in` and return a string representation.
What I don't like with both of these APIs is that they ask the object how
to render itself. Applications often have several different ways to represent
the same object based on the context, e.g. frontend vs admin panel, or
HTML vs JSON.
It would be way more powerful if the view or controller was in charge of
selecting the proper renderer for a given object.
e.g. (pseudo code)
```ruby
module Admin
class BaseController < ApplicationController
def renderer_for(object)
{ partial: "admin/#{object.class.underscore}" }
end
end
```
On paper, you'd probably want to define this on thew view, but
in practive Rails users very rarely interact with the view, hence
why I do this on the controller. But I'm maybe missing something.
Another benefit of such API would be to allow view component systems
to better integrate with Rails by implementing their own conventions,
e.g. (pseudo code)
```
module MyViewComponentExtension
def renderer_for(object)
component_name = "#{object.class.name}Component"
"#{self.class.module_parent_name}#{component_name}".safe_constantize ||
component_name.constantize
end
end
```
It's somewhat similar to what Active Model Serializer used to do for
its default serializer system:
https://github.com/rails-api/active_model_serializers/blob/0-9-stable/lib/action_controller/serialization.rb
It used to work quite well.
|
I like this proposal but the only thing that bothers me is dropping the In that sense, I would prefer if the API was similar to: class ApplicationController
def renderer_for(partial_path)
if should_be_rendered_by_action_view?(partial_path)
{ partial: partial_path }
else
find_renderer_for(partial_path)
end
end
end |
|
@paracycle part of the reason is that not all rendering system are tied to file system paths, e.g. ViewComponent, Active Model Serializers, etc. I think we should try to support these better. |
Follow-up to [rails#46202][] Without overriding the new `#render_in` method, previous behavior will be preserved: render the view partial determined by calling `#to_partial_path`, then pass `object: self`. With the following view partial: ```erb <%# app/views/people/_person.html.erb %> <% local_assigns.with_defaults(shout: false) => { shout: } %> <%= shout ? person.name.upcase : person.name %> ``` Callers can render an instance of `Person` as a positional argument or a `:renderable` option: ```ruby person = Person.new(name: "Ralph") render person # => "Ralph" render person, shout: true # => "RALPH" render renderable: person # => "Ralph" render renderable: person, locals: { shout: true } # => "RALPH" ``` This preserves backward compatibility. At the same time, the `#render_in` method provides applications with an more flexibility, and an opportunity to manage how to transform models into Strings. For example, users of ViewComponent can map a model directly to a `ViewComponent::Base` instance. [rails#46202]: rails#46202 (comment)
Follow-up to [rails#46202][] Without overriding the new `#render_in` method, previous behavior will be preserved: render the view partial determined by calling `#to_partial_path`, then pass `object: self`. With the following view partial: ```erb <%# app/views/people/_person.html.erb %> <% local_assigns.with_defaults(shout: false) => { shout: } %> <%= shout ? person.name.upcase : person.name %> ``` Callers can render an instance of `Person` as a positional argument or a `:renderable` option: ```ruby person = Person.new(name: "Ralph") render person # => "Ralph" render person, shout: true # => "RALPH" render renderable: person # => "Ralph" render renderable: person, locals: { shout: true } # => "RALPH" ``` This preserves backward compatibility. At the same time, the `#render_in` method provides applications with an more flexibility, and an opportunity to manage how to transform models into Strings. For example, users of ViewComponent can map a model directly to a `ViewComponent::Base` instance. [rails#46202]: rails#46202 (comment)
Follow-up to [rails#46202][] Without overriding the new `#render_in` method, previous behavior will be preserved: render the view partial determined by calling `#to_partial_path`, then pass `object: self`. With the following view partial: ```erb <%# app/views/people/_person.html.erb %> <% local_assigns.with_defaults(shout: false) => { shout: } %> <%= shout ? person.name.upcase : person.name %> ``` Callers can render an instance of `Person` as a positional argument or a `:renderable` option: ```ruby person = Person.new(name: "Ralph") render person # => "Ralph" render person, shout: true # => "RALPH" render renderable: person # => "Ralph" render renderable: person, locals: { shout: true } # => "RALPH" ``` This preserves backward compatibility. At the same time, the `#render_in` method provides applications with an more flexibility, and an opportunity to manage how to transform models into Strings. For example, users of ViewComponent can map a model directly to a `ViewComponent::Base` instance. [rails#46202]: rails#46202 (comment)
|
@byroot I've opened #50623 to expand the capabilities of the On that heels of that change, I've also started to explore expanding
I relate to this concern. The motivation behind making options available to From an Action View perspective, that information could enable some flexibility. From a third-party integration perspective (like ViewComponent or Active Model Serializers), then information could be used to make similar encoding decisions. These proposed changes are still at a different application layer than this PR (#46202), but they share similar goals. |
Follow-up to [rails#46202][] Without overriding the new `#render_in` method, previous behavior will be preserved: render the view partial determined by calling `#to_partial_path`, then pass `object: self`. With the following view partial: ```erb <%# app/views/people/_person.html.erb %> <% local_assigns.with_defaults(shout: false) => { shout: } %> <%= shout ? person.name.upcase : person.name %> ``` Callers can render an instance of `Person` as a positional argument or a `:renderable` option: ```ruby person = Person.new(name: "Ralph") render person # => "Ralph" render person, shout: true # => "RALPH" render renderable: person # => "Ralph" render renderable: person, locals: { shout: true } # => "RALPH" ``` This preserves backward compatibility. At the same time, the `#render_in` method provides applications with an more flexibility, and an opportunity to manage how to transform models into Strings. For example, users of ViewComponent can map a model directly to a `ViewComponent::Base` instance. [rails#46202]: rails#46202 (comment)
Follow-up to [rails#46202][] Without overriding the new `#render_in` method, previous behavior will be preserved: render the view partial determined by calling `#to_partial_path`, then pass `object: self`. With the following view partial: ```erb <%# app/views/people/_person.html.erb %> <% local_assigns.with_defaults(shout: false) => { shout: } %> <%= shout ? person.name.upcase : person.name %> ``` Callers can render an instance of `Person` as a positional argument or a `:renderable` option: ```ruby person = Person.new(name: "Ralph") render person # => "Ralph" render person, shout: true # => "RALPH" render renderable: person # => "Ralph" render renderable: person, locals: { shout: true } # => "RALPH" ``` This preserves backward compatibility. At the same time, the `#render_in` method provides applications with an more flexibility, and an opportunity to manage how to transform models into Strings. For example, users of ViewComponent can map a model directly to a `ViewComponent::Base` instance. [rails#46202]: rails#46202 (comment)
Follow-up to [rails#46202][] Without overriding the new `#render_in` method, previous behavior will be preserved: render the view partial determined by calling `#to_partial_path`, then pass `object: self`. With the following view partial: ```erb <%# app/views/people/_person.html.erb %> <% local_assigns.with_defaults(shout: false) => { shout: } %> <%= shout ? person.name.upcase : person.name %> ``` Callers can render an instance of `Person` as a positional argument or a `:renderable` option: ```ruby person = Person.new(name: "Ralph") render person # => "Ralph" render person, shout: true # => "RALPH" render renderable: person # => "Ralph" render renderable: person, locals: { shout: true } # => "RALPH" ``` This preserves backward compatibility. At the same time, the `#render_in` method provides applications with an more flexibility, and an opportunity to manage how to transform models into Strings. For example, users of ViewComponent can map a model directly to a `ViewComponent::Base` instance. [rails#46202]: rails#46202 (comment)
Follow-up to [rails#46202][] Without overriding the new `#render_in` method, previous behavior will be preserved: render the view partial determined by calling `#to_partial_path`, then pass `object: self`. With the following view partial: ```erb <%# app/views/people/_person.html.erb %> <% local_assigns.with_defaults(shout: false) => { shout: } %> <%= shout ? person.name.upcase : person.name %> ``` Callers can render an instance of `Person` as a positional argument or a `:renderable` option: ```ruby person = Person.new(name: "Ralph") render person # => "Ralph" render person, shout: true # => "RALPH" render renderable: person # => "Ralph" render renderable: person, locals: { shout: true } # => "RALPH" ``` This preserves backward compatibility. At the same time, the `#render_in` method provides applications with an more flexibility, and an opportunity to manage how to transform models into Strings. For example, users of ViewComponent can map a model directly to a `ViewComponent::Base` instance. [rails#46202]: rails#46202 (comment)
Follow-up to [rails#46202][] Without overriding the new `#render_in` method, previous behavior will be preserved: render the view partial determined by calling `#to_partial_path`, then pass `object: self`. With the following view partial: ```erb <%# app/views/people/_person.html.erb %> <% local_assigns.with_defaults(shout: false) => { shout: } %> <%= shout ? person.name.upcase : person.name %> ``` Callers can render an instance of `Person` as a positional argument or a `:renderable` option: ```ruby person = Person.new(name: "Ralph") render person # => "Ralph" render person, shout: true # => "RALPH" render renderable: person # => "Ralph" render renderable: person, locals: { shout: true } # => "RALPH" ``` This preserves backward compatibility. At the same time, the `#render_in` method provides applications with an more flexibility, and an opportunity to manage how to transform models into Strings. For example, users of ViewComponent can map a model directly to a `ViewComponent::Base` instance. [rails#46202]: rails#46202 (comment)
Context
There has been some demands from view systems not based on paths, to offer some extension API that are better suited for them: https://discuss.rubyonrails.org/t/replace-to-partial-path-calls-with-a-more-abstract-to-renderable/81457
I'm in favor of the general idea, but not at all with the proposed solutions so far.
Existing API
Currently you can customize the behavior of
render some_objectin two ways:to_partial_pathand return a view path.render_inand return a string representation.What I don't like with both of these APIs is that they ask the object how to render itself. Applications often have several different ways to represent the same object based on the context, e.g. frontend vs admin panel, or HTML vs JSON.
Proposal
It would be way more powerful if the view or controller was in charge of selecting the proper renderer for a given object.
e.g. (pseudo code)
On paper, you'd probably want to define this on thew view, but in practive Rails users very rarely interact with the view, hence why I do this on the controller. But I'm maybe missing something.
Another benefit of such API would be to allow view component systems to better integrate with Rails by implementing their own conventions, e.g. (pseudo code)
It's somewhat similar to what Active Model Serializer used to do for its default serializer system:
https://github.com/rails-api/active_model_serializers/blob/0-9-stable/lib/action_controller/serialization.rb
It used to work quite well.
Note
his is purely a proof of concept to demonstrate the general idea, what capabilities it brings to the table.
I'm absolutely not happy with that implementation and I'm not suggesting to merge it. If we chose to go with the idea, we
should carefully study what the API would look like.