Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce support for 3rd-party component frameworks #36388

Merged
merged 2 commits into from Jun 13, 2019
Merged

Introduce support for 3rd-party component frameworks #36388

merged 2 commits into from Jun 13, 2019

Conversation

joelhawksley
Copy link
Contributor

@joelhawksley joelhawksley commented Jun 3, 2019

Note: This PR initially was titled: Introduce support for ActionView::Component. I've updated the content to better reflect the changes we ended up shipping, to use the new name of GitHub's library, ViewComponent, and to remove mentions of validations, which we no longer use. - @joelhawksley, March 2020

Introduce support for 3rd-party component frameworks

This PR introduces structural support for 3rd-party component frameworks, including ViewComponent, GitHub's framework for building view components.

Specifically, it modifies ActionView::RenderingHelper#render to support passing in an object to render that responds_to render_in, enabling us to build view components as objects in Rails.

We’ve been running a variant of this patch in production at GitHub since March, and now have about a dozen components used in over a hundred call sites.

The PR includes an example component (TestComponent) that closely resembles the base component we're using at GitHub.

I spoke about our project at RailsConf, where we got lots of great feedback from the community. Several folks asked us to upstream it into Rails.

Why

In working on views in our Rails monolith at GitHub (which has over 4,000 templates), we have run into several key pain points:

Testing

Currently, Rails encourages testing views via integration or system tests. This discourages us from testing our views thoroughly, due to the costly overhead of exercising the routing/controller layer, instead of just the view. It also often leads to partials being tested for each view they are included in, cheapening the benefit of DRYing up our views.

Code Coverage

Many common Ruby code coverage tools cannot properly handle coverage of views, making it difficult to audit how thorough our tests are and leading to gaps in our test suite.

Data Flow

Unlike a method declaration on an object, views do not declare the values they are expected to receive, making it hard to figure out what context is necessary to render them. This often leads to subtle bugs when we reuse a view across different contexts.

Standards

Our views often fail even the most basic standards of code quality we expect out of our Ruby classes: long methods, deep conditional nesting, and mystery guests abound.

ViewComponent

ViewComponent is an effort to address these pain points in a way that improves the Rails view layer.

Building Components

Components are subclasses of ViewComponent and live in app/components.

They include a sidecar template file with the same base name as the component.

Example

Given the component app/components/test_component.rb:

class TestComponent < ViewComponent
  def initialize(title:)
    @title = title
  end
end

And the template app/components/test_component.html.erb:

<span title="<%= @title %>"><%= content %></span>

We can render it in a view as:

<%= render(TestComponent.new(title: "my title")) do %>
  Hello, World!
<% end %>

Which returns:

<span title="my title">Hello, World!</span>

Testing

Components are unit tested directly, based on their HTML output. The render_inline test helper enables the use of Capybara assertions:

def test_render_component
  render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }

  assert_text "Hello, World"
  assert_selector "span[title='my title']"
end

Benefits

Testing

ViewComponent allows views to be unit-tested. Our unit tests run in around 25ms/test, vs. ~6s/test for integration tests.

Code Coverage

ViewComponent is at least partially compatible with code coverage tools. We’ve seen some success with SimpleCov.

Data flow

By clearly defining the context necessary to render a component, we’ve found them to be easier to reuse than partials.

Existing implementations

ViewComponent is far from a novel idea. Popular implementations of view components in Ruby include, but are not limited to:

In action

I’ve created a demo repository pointing to this branch.

Co-authored-by

A cross-functional working group of engineers and members of our Design Systems team collaborated on this work, including by not limited to: @natashaU, @tenderlove, @shawnbot, @emplums, @broccolini, @jhawthorn, @myobie, and @zawaideh.

Additionally, numerous members of the community have shared their ideas for ViewComponent, including but not limited to: @cgriego, @xdmx, @skyksandr, @jcoyne, @dylanahsmith, @kennyadsl, @cquinones100, @erikaxel, @zachahn, and @trcarden.

@rails-bot rails-bot bot added the actionview label Jun 3, 2019
@hayesr
Copy link
Contributor

hayesr commented Jun 3, 2019

2¢ on Template architecture …
I agree that inline templates don't feel right. I'd love to see some kind of convention for matching template files.

Without having used this, my estimation is that it would be nice to have an app/components directory that would follow controller naming & namespacing conventions. I suppose there has to be an app/views/components directory. eg

# components
app/components/foo.rb
app/components/billing/total.rb

# corresponding views
app/views/components/_foo.html.erb
app/views/components/billing/_total.html.erb

Then, a method we could overwrite to make it dynamic would be super cool

class MyComponent < ActionView::Component::Base
  def partial_name
    super + "-#{object.status}"
  end
end
app/views/components/_my_component-pending.html.erb
app/views/components/_my_component-active.html.erb

@compiled = true

instance = new(*args)
instance.content = view_context.capture(&block) if block_given?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like it if we could pass this in to new rather than using a setter. I know it's to do with the superclass not matching signatures, but I wanted to comment with that anyway. Using a setter like this will complicate downstream uses because they'll need to know to call the setter. On the flip side, making it a required parameter to initialize will teach people it's required because they won't be able to construct the object without it. Plus you don't even need to read the docs to figure that out! 😊

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I'm not opposed to that approach, but I wonder how it might look for components that don't render content.

Right now they just ignore the nil content accessor, but if we were always passing a content argument wouldn't they need to explicitly no-op that argument? Like so:

def initialize(_content:); end


module ActionView
class Component < ActionView::Base
include ActiveModel::Validations
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want this by default? Validating all components will punish performance of components that have no validations. If most components require validation, it probably makes sense. I just don't have a good feeling of the requirement.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe one more layer called class ApplicationComponent < ActionView::Component?
That will be the place to specify template handler as well as including ActiveModel::Validations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My gut says it would be best to measure the impact of having validations enabled by default.

If it's not much overhead, I think this would be a nice default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@skyksandr I 💯agree about having an ApplicationComponent. We already follow this pattern internally and have found it useful.

@skyksandr
Copy link
Contributor

Speaking about variants:

Inline templates and/or Sinatra style

Pros: markup and methods close to each other
Cons: should we use inheritance to override it?

If you're not familiar with Sinatra's inline templates:

class OurComponent
  def hello_message
    "Hello, world!"
  end

  ...
end

__END__

@@ template
<div class="title"><%= hello_message %></div>

We can even drop @@ template

class OurComponent;  ...; end

__END__

<div class="title"><%= hello_message %></div>

Sidecar

I like the sidecar approach, would only ask to consider placing files as close in the directory structure as possible, like:

app/views/components/
  - my_component.rb
  - my_component.html.erb
  - my_component.html+mobile.erb

or

app/views/components/
  - my_component/
    - component.rb
    - template.html.erb
    - template.html+mobile.erb

@tenderlove
Copy link
Member

I really like @skyksandr's proposal of:

app/views/components/
  - my_component/
    - component.rb
    - template.html.erb
    - template.html+mobile.erb

Seems like it would make packaging and distribution easy. I imagine you could have the same directory structure inside a gem just inside a lib folder.

@jonathanhefner
Copy link
Member

We’re not sure if the current integration point in Rails (ActionView::RenderingHelper#render) is the best option.

To me, the API that komponent implements feels a bit more Railsy:

<%= component 'button', text: 'My button' %>

Namely, using a method other than render allows passing a String or Symbol (rather than a Class) to designate the component. It could also offer the possibility of positional args, where appropriate:

<%= component :section, "Some Label" do %>
  Some content.
<% end %>

@joelhawksley
Copy link
Contributor Author

Thanks for the feedback folks!

Since posting this PR, we've found ourselves wanting the template to be in a separate file.

Personally, I prefer @skyksandr's suggestion of:

app/views/components/
  - my_component.rb
  - my_component.html.erb
  - my_component.html+mobile.erb

As it feels a little heavy-handed to have one folder per component, especially if components can still define their templates inline in the same file.

Namely, using a method other than render allows passing a String or Symbol (rather than a Class) to designate the component.

One of the advantages to the current approach of passing a class name (TestComponent) to render is that we are performing an unambiguous class lookup. There is a significant amount of complexity/overhead in the way that Rails looks up templates via a symbol or a string.

More broadly, whether we use the existing render entrypoint is as much a functional question as it is a philosophical one:

Do we view this new architecture as something that should be thought of as a part of the existing Rails rendering functionality, or something entirely new? I feel like integrating with the existing view rendering mental model would make this new architecture easier to reason about for other developers.

@joelhawksley
Copy link
Contributor Author

Then, a method we could overwrite to make it dynamic would be super cool

class MyComponent < ActionView::Component::Base
  def partial_name
    super + "-#{object.status}"
  end
end
app/views/components/_my_component-pending.html.erb
app/views/components/_my_component-active.html.erb

@hayesr one of the optimizations the currently proposed architecture gives us is the ability to easily precompile the ERB ahead of time, something that is currently difficult to do with existing Rails templates due to how formats/locals/etc can vary.

I'm curious what the use case would be for having separate templates for different status values in this example, instead of having a single template for all statuses.

Perhaps we could find a middle ground where we allowed dynamic template lookup, but pre-compiled all the templates that matched the name of the component ahead of time?

@tenderlove discussed this issue, and how we currently work around it at GitHub by always referencing fully qualified template paths, in his RailsConf keynote this year: https://youtu.be/8Dld5kFWGCc?t=1857

@jonathanhefner
Copy link
Member

One of the advantages to the current approach of passing a class name (TestComponent) to render is that we are performing an unambiguous class lookup. There is a significant amount of complexity/overhead in the way that Rails looks up templates via a symbol or a string.

I absolutely agree that the performance benefits are a major selling point. But I think we could retain those benefits with a component registry. It could be a simple Hash (or HWIA) that's populated on app startup. It might also be a good place to trigger template pre-compilation.

@hayesr
Copy link
Contributor

hayesr commented Jun 6, 2019

@joelhawksley I would defer to the optimization. I was thinking about something I'm working on, a "status widget" similar to the GitHub issue badge, but with more potential for branching logic. And I prefer distinct templates to one template with a lot of <% if %> <% else %>. (Like your RailsConf talk example)

—I don't think a middle ground is necessary. Probably my use case would be better served by switching components rather than templates.

Now I've got new ideas 😁What happens when one Component inherits from another? Does the template path work like it does with Controllers? (I would vote yes please) For example one might expect that templates defined by SuperComponent would be used by SubComponent but that _sub_component.html+tablet.erb would override _super_component.html+tablet.erb

To the question of 1-folder-per-component, would it kill the pre-compilation to make it possible to override the directory but not the template name? I agree that it's a little heavy to have a folder for every component, but I think that app/views/components directory is going to get awfully cluttered. My preference would be to have folders, and I would set those explicitly in each Component class given the ability.

@zachahn
Copy link
Contributor

zachahn commented Jun 6, 2019

I also kinda like the idea of a flat structure in app/views/components! And I also really like how this implementation uses class names instead of strings/symbols for lookup. Since I'm pretty familiar with the general rules of how Rails autoloads classes, it feels very straightforward to me :)

If components each had their own directories, would the class definition of the component look something like MyComponent::Component in the example we're talking about? (I personally feel more comfortable with a class name like ButtonComponent instead of Button::Component since the latter might have a hard time resolving if the Button class already existed somewhere in the application lol)

@nogtini
Copy link

nogtini commented Jun 6, 2019

To go with the analogy of container/presentational components, how would the container-prefixed components handle fetching? Or is this left up to the controller? If so, wouldn't embedding components inside /views/components effectively make all components presentational/presentational components if they're delegating behavior management to the controller?

I mean this only in the sense that having a type of component that delegates behavior to the controller and is used to render presentational components seems to me very different than the React ecosystem where every component is effectively view and controller bundled together, all the way down.

If, on the other hand, the container components are in fact views that fetch and manage their own state, wouldn't that be pushing the boundaries of what it means to be a view and perhaps need at least a directory promotion outside of "all components are views"?

@baweaver
Copy link
Contributor

baweaver commented Jun 9, 2019

Very fond of the idea, it opens up a lot of power in Rails.

One thing I'd be curious about, would it be possible to drop the render and imply it? Currently it's:

<%= render(TestComponent, title: "my title" ) do %>
  Hello, World!
<% end %>

Some ideas:

<!-- Class-based -->
<%= TestComponent.new(title: 'something') do %>
  Hello World!
<% end %>

<!-- Class-based, array accessor style -->
<%= TestComponent[title: 'something'] do %>
  Hello World!
<% end %>

The reason I ask is because if they retain that style it may make it more intuitive to have nested components:

<%= GridComponent[title: 'something'] do %>
  <%= Tile[name: 'something', image: 'src'] %>
<% end %>

Then in a file:

def tiles
  @tiles.map { |attrs| Tile[attrs] }
end

def self.template
  <<~'erb'
    <div><%= tiles %></div>
  erb
end

It could also potentially encapsulate some of the testing behavior from inferring the render, making a test potentially look like this:

def test_render_component
  assert_equal(
    %(<span title="my title">Hello, World!</span>),
    TestComponent[title: "my title"] { "Hello, World!" }.to_html
  )
end

Though stylistically this gets a bit towards more of what one would see in a PORO-oriented design, just a few ideas and musings.

@rmacklin
Copy link
Contributor

rmacklin commented Jun 9, 2019

One thing I'd be curious about, would it be possible to drop the render and imply it?

Some ideas:

<!-- Class-based -->
<%= TestComponent.new(title: 'something') do %>
  Hello World!
<% end %>

<!-- Class-based, array accessor style -->
<%= TestComponent[title: 'something'] do %>
  Hello World!
<% end %>

I like that [] syntax ("class-based, array accessor style") for the same reasons I talked about a similar alternative in ViewComponent/demo#1 (comment):

While I don’t mind the render Issues::Badge syntax, we also adopted a syntax backed by helper methods, which turns:

<%= render PullRequestBadge, state: issue.pull_request.state.to_sym, is_draft: issue.pull_request.draft? %>

into something like:

<%= pull_request_badge state: issue.pull_request.state.to_sym, is_draft: issue.pull_request.draft? %>

One thing we liked about this syntax is that it looks very much like a custom element (if you squint, you might not see a difference 😉). To that end, we even started using a pattern of accepting additional keyword arguments to be passed directly as DOM attributes in the helpers (in addition to the explicit arguments). So, for instance:

<%= issue_badge state: issue.state.to_sym, id: 'some_id', 'data-foo': 'bar' %>

would render the Issues::Badge component and pass along the id and data-foo attributes onto the (root) rendered element.

I pulled two small examples from our app into rmacklin/components_in_rails (the helpers are in app/helpers/components) and rendered them in a very bare bones style guide:
https://github.com/rmacklin/components_in_rails/blob/master/app/views/home/index.html.erb
with these examples:

style_guide/_button_group_example.html.erb

<%= button_group do |button_group| %>
  <%= button_group.button('Cat in the Hat', active: true, class: 'js-custom-class foobar') %>
  <%= button_group.button do %>
    <span class="fa fa-pencil"></span> Bartholomew and the Oobleck
  <% end %>
  <%= button_group.button('Yertle the Turtle', :'data-foo' => 'bar') %>
<% end %>

style_guide/_help_bubble_example.html.erb

<%= help_bubble(
  'You will need to restart this move in',
  accompanying_text: 'Looking for a different property?',
  direction: 'ne',
  id: 'different_property_help_bubble'
) %>

Here are a few more examples...

<%= options_dropdown label: 'Actions' do |dropdown| %>
  <%= dropdown.item 'Dropdown Item 1' %>
  <%= dropdown.item_link 'Dropdown Item 2', url: 'http://www.example.com' %>
<% end %>
<%= datapair key: 'Custom Class', value: 'Inspect Me', id: 'datapair_with_id', class: 'js-datapair' %>
<%= expandable_section title: 'title', initial_state: 'collapsed' do %>
  <div>Content</div>
<% end %>
<%= banner_alert title: 'Something', subtitle: 'I am a subtitle' do %>
  <%# ... %>
<% end %>

A more involved component looks like:

<%= filterable_collection do |fc| %>
  <%= fc.filter_box instructions: 'Enter in your filters below.' do %>
    <%= simple_form_for model.new, url: '/' do |f| %>
      <%# ... %>
      <% end %>
      <div class="btn-toolbar">
        <%= f.submit 'Search', class: 'btn btn-primary', :'data-disable-with' => 'Please Wait...' %>
      </div>
    <% end %>
  <% end %>

  <%= fc.results html_options: { id: 'results' } do %>
    <%# Initial results go here %>
  <% end %>
<% end %>

Given a component class such as MyComponent, the method name could be inferred from the class name by default (my_component), but any class could also choose to explicitly name its corresponding view_context method (e.g. Issues::Badge could define issue_badge if it made sense for the view method to be slightly different than the inferred default, or MyComponent could change its view method to deprecated__my_component when we've deprecated it).

We could also make it configurable whether the methods are mixed into the view context directly (which has a larger potential for naming collisions with other methods/locals) or onto a more granular scope where they could be called on an explicit receiver (which could come with a short alias), e.g.

<%= c.pull_request_badge state: issue.pull_request.state.to_sym, is_draft: issue.pull_request.draft? %>

This pseudo-custom-element syntax was appealing to our team, so we started using it a lot in our rails views.

@rmacklin
Copy link
Contributor

rmacklin commented Jun 9, 2019

I really like @skyksandr's proposal of:

app/views/components/
  - my_component/
    - component.rb
    - template.html.erb
    - template.html+mobile.erb

Seems like it would make packaging and distribution easy. I imagine you could have the same directory structure inside a gem just inside a lib folder.

We were thinking about distribution, too. I could imagine we may also want to distribute CSS (or SCSS) with the component, and potentially even associated javascript (to progressively enhance the component, e.g. with an associated stimulus controller). With sprockets, gems can distribute CSS/JS in lib/assets/, but it'd be nice for the associated stylesheet and javascript module to be co-located with the template and ruby class in lib/my_component/. That said, a lot of apps have replaced sprockets with node-based front end bundlers, so realistically these components might be distributed through a gem and a corresponding node module.

@jaredcwhite
Copy link

I'm really digging this PR so far. I want to echo @rmacklin 's comment about bundling assets—in particular, I could see a serious use case here where the template format is something that could be parsed client-side as well, in which case a bundled Stimulus controller could import the template via Webpack and update a component client-side with the same template the server-rendered component uses. (The "holy grail" as it were…) As it is, I often find myself building a "component" as either a server-rendered partial OR a Stimulus controller + HTML template on the client, and that approach doesn't feel as Rails-y as I would like.

But even a basic first pass at this PR would be awesome.

@rmacklin
Copy link
Contributor

rmacklin commented Jun 9, 2019

update a component client-side with the same template the server-rendered component uses. (The "holy grail" as it were…)

I was having the exact same thoughts/dreams (having previously used mustache for shared client/server templates), but worried it might be getting too far out there. Hearing someone else say it is at least a little reassuring, heh

@dubadub
Copy link

dubadub commented Jun 9, 2019

Components in Rails is the most desirable feature I'd like to see. Thank you very much for making that effort and bringing that topic to the surface, @joelhawksley.

Rails still has its power, and server-rendered apps solve many use cases. I think, for 80% apps, using SPA is overkill and Rails has its place under the sun and will have it for a long time. Also, now, when there is Stimulus which is elegant, simple yet powerful, I'm excited to see that the community wants to move in that direction. If Rails has a right componentisation approach, it will make my experience much better.

My Pain Points

  • Low transparency on the dependencies makes my new feature dev experience worse. CSS, JS and images are very detached from the context they're used. I need to switch a lot in file tree; things are easy to collide, peer developers don't know what's already been done if there are no strong naming conventions.
  • Low transparency on the dependencies pollutes asset files with dead code. The dependency of template from used css and js is not explicit. Often dead assets remain in the codebase because I forgot to delete them when I deleted a view.

Well, the point is one - low transparency.

It could be the result of my poor abilities to control, but I think Rails can make this work for us. At the end of the day, Rails puts developers' experience first.

How I'd like to see it solved

In my ideal world, I'd like to see my components defined with related JS, CSS, images, other assets and template in one place. The ruby part of it seems to be unnecessary and should be optional.

In my views, I will have templates related to controller actions, but the sole purpose of these will be to use existing components to build that particular page. Like that:

# app/views/pages/home.slim
= c("layout/main--wide") do
    = c("home/hero")
    = c("home/cta", user: @current_user)
    = c("home/how")
    = c("home/facts")

Ideally, it shouldn't have references any JS or css classes and only declares a page structure.

In component, I will have my template, JS, CSS:

app/views/components/
  - layout/
    - main--wide.html.slim
    - main.html.slim
    - layout.scss
    - layout.js
    - logo.png
  - home/
  	- hero/
  		- hero.html.slim
  		- hero.scss
  	...

We can assume that everything inside the component directory is needed.

And the really cool thing to do would be to add some magic in it.

When we are building assets, we know router endpoints, and for each endpoint, we can find related view and generate components tree.

Based on the tree and config, we can define rules to build our packs declaratively:

pack path: /^admin/, name: :admin
pack path: /^public/, name: :public
pack path: /^internal/, name: :internal

Then, during assets building, it will check all the routes starting with admin and build all the resources for production.

In runtime, it will serve a pack depending on the path.

It's not impossible, is it?

PS.

I spent some time playing with the approach taken by https://evilmartians.com/chronicles/evil-front-part-1, and as I know, it was inspired by komponent gem. I got rid of the Sprocket pipeline altogether, have a template, css, js (stimulusjs controllers), images located together. It is working very well. The downside of it is that I need to manually provide all the dependencies for JS, CSS, and set up webpack properly (it was painful). I can make a public example of the approach if there is interest.

@pixeltrix
Copy link
Contributor

I assume that we'd want to support a ApplicationComponent in a similar fashion to how we have ApplicationModel, ApplicationJob, etc. ?

@paxer
Copy link

paxer commented Jun 11, 2019

this is a great proposal as a starting point, what I would personally like to see in Component feature is a Component has own isolated Javascript and CSS attached to it. JS and CSS could be optional but it is very handy to have it in one place as well as namespace CSS by component name automagically can make things even nicer.

so

app/views/components/
  - my_component/
    - component.js
    - component.css
    - component.rb
    - template.html.erb
    - template.html+mobile.erb

@ravicious
Copy link

Just FYI regarding the presentational/container component approach by Dan Abramov, the linked blogpost starts with a disclaimer:

Update from 2019: I wrote this article a long time ago and my views have since evolved. In particular, I don’t suggest splitting your components like this anymore. If you find it natural in your codebase, this pattern can be handy. But I’ve seen it enforced without any necessity and with almost dogmatic fervor far too many times. The main reason I found it useful was because it let me separate complex stateful logic from other aspects of the component. Hooks let me do the same thing without an arbitrary division. This text is left intact for historical reasons but don’t take it too seriously.

I see how this pattern could've been useful for naming components in your app. OTOH, I think it should be possible to write appropriate naming guidelines without directly mentioning the concept of presentational & container components so that people don't go overboard with them like they did in Redux.

@jaredbeck
Copy link
Contributor

jaredbeck commented Jun 11, 2019

Templating engine support
We should probably support arbitrary templating engines like normal Rails views.

Yes please. Eg. HAML is quite popular. Napkin math: downloads since late 2017: haml / rails = 7M / 45M = 20% of community uses haml (+/- the usual concerns about download counts)

* Fix `select_tag` so that it doesn't change `options` when `include_blank` is present.

*Younes SERRAJ*


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new line was intentional 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops! Resolved!

…der_in`

Co-authored-by: Natasha Umer <natashau@github.com>
Co-authored-by: Aaron Patterson <tenderlove@github.com>
Co-authored-by: Shawn Allen <shawnbot@github.com>
Co-authored-by: Emily Plummer <emplums@github.com>
Co-authored-by: Diana Mounter <broccolini@github.com>
Co-authored-by: John Hawthorn <jhawthorn@github.com>
Co-authored-by: Nathan Herald <myobie@github.com>
Co-authored-by: Zaid Zawaideh <zawaideh@github.com>
Co-authored-by: Zach Ahn <engineering@zachahn.com>
@damien-roche
Copy link

damien-roche commented Jun 13, 2019

I have used trailblazer/cells for this structure and have also started to package assets.

This works very well with Vue to progressively enhance server-rendered components, or direct Vue templates. The main complication I've found is with regards to splitting assets across app/assets and components/assets, some even have app/javascripts where an SPA lives. Would be great to see some standardisation around this.

I am still using Rails views but mostly to render the components. In the Rails view I might set title, etc, pass in data from instance variables in the controller into the component.

I usually break it up into contexts (similar to DDD):

app/cells/
  users/
    assets/
    views/

trailblazer/cells also comes with a decent caching layer which is nice.

Regardless, this structure is infinitely better than the current mess of controller/views/partials which don't seem to offer very much by way of encapsulation. I want to be in Ruby doing OOP.

Hope these components are officially integrated and supported soon! One of the biggest resistances I have with using cells et al is onboarding other developers. It would be great if we could point to the Rails docs, and if most Rails devs were already familiar with this concept of Components, where to find them, how to build them, test, etc

@sebyx07
Copy link

sebyx07 commented Jun 13, 2019

Shouldn't we get inspired by https://hyperstack.org to write HTML in ruby?
Inspiration from https://github.com/ebryn/ember-component-css

And for style, ruby supports the syntax to generate css. Inspiration from https://github.com/ebryn/ember-component-css

def style
{
 '.label': { 'text-align': 'center' }
}
end

then the output Inspiration from https://github.com/ebryn/ember-component-css

.my-component-32131321 .label{
...
}

Also more integrations with stimulus would be nice!

seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 8, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 8, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 8, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 9, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 9, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 9, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 9, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 9, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 10, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 10, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 10, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 10, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 11, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 12, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 12, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 12, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 12, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Jan 30, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Feb 22, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Feb 22, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Mar 2, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Mar 2, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Mar 7, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Mar 8, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Mar 24, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Apr 5, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Apr 18, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Apr 18, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
seanpdoyle added a commit to seanpdoyle/rails that referenced this pull request Apr 24, 2024
Closes [rails#45432][]

Support for objects that respond to `#render_in` was introduced in
[rails#36388][] and [rails#37919][]. Those implementations assume that the
instance will all the context it needs to render itself. That assumption
doesn't account for call-site arguments like `locals: { ... }` or a
block.

This commit expands support for rendering with a `:renderable` option to
incorporate locals and blocks. For example:

```ruby
class Greeting
  def render_in(view_context, **options, &block)
    if block
      view_context.render plain: block.call
    else
      case Array(options[:formats]).first
      when :json
        view_context.render json: { greeting: "Hello, World!" }
      else
        view_context.render **options, inline: "<%= Hello, <%= name %>!"
      end
    end
  end

  def format
    :html
  end
end

render(Greeting.new)                              # => "Hello, World!"
render(Greeting.new, name: "Local")               # => "Hello, Local!"
render(Greeting.new) { "Hello, Block!" }          # => "Hello, Block!"

render(renderable: Greeting.new, formats: :json)  # => "{\"greeting\":\"Hello, World!\"}"
```

Since existing tools depend on the `#render_in(view_context)` signature
without options, this commit deprecates that signature in favor of one
that accepts options and a block.

[rails#45432]: rails#45432
[rails#36388]: rails#36388
[rails#37919]: rails#37919
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet