Join GitHub today
GitHub is home to over 40 million developers working together to host and review code, manage projects, and build software together.
Sign upIntroduce support for ActionView::Component #36388
Conversation
merge master
8e06178
to
1f261e0
This comment has been minimized.
This comment has been minimized.
|
2¢ on Template architecture … Without having used this, my estimation is that it would be nice to have an # components
app/components/foo.rb
app/components/billing/total.rb
# corresponding views
app/views/components/_foo.html.erb
app/views/components/billing/_total.html.erbThen, 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
endapp/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? |
This comment has been minimized.
This comment has been minimized.
tenderlove
Jun 3, 2019
Member
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!
This comment has been minimized.
This comment has been minimized.
joelhawksley
Jun 5, 2019
Author
Contributor
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 |
This comment has been minimized.
This comment has been minimized.
tenderlove
Jun 3, 2019
Member
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.
This comment has been minimized.
This comment has been minimized.
skyksandr
Jun 4, 2019
Maybe one more layer called class ApplicationComponent < ActionView::Component?
That will be the place to specify template handler as well as including ActiveModel::Validations
This comment has been minimized.
This comment has been minimized.
joelhawksley
Jun 5, 2019
Author
Contributor
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.
This comment has been minimized.
This comment has been minimized.
joelhawksley
Jun 5, 2019
Author
Contributor
@skyksandr I ApplicationComponent. We already follow this pattern internally and have found it useful.
This comment has been minimized.
This comment has been minimized.
skyksandr
commented
Jun 4, 2019
|
Speaking about variants: Inline templates and/or Sinatra stylePros: markup and methods close to each other 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 class OurComponent; ...; end
__END__
<div class="title"><%= hello_message %></div>SidecarI like the sidecar approach, would only ask to consider placing files as close in the directory structure as possible, like:
or
|
This comment has been minimized.
This comment has been minimized.
|
I really like @skyksandr's proposal of:
Seems like it would make packaging and distribution easy. I imagine you could have the same directory structure inside a gem just inside a |
This comment has been minimized.
This comment has been minimized.
To me, the API that komponent implements feels a bit more Railsy: <%= component 'button', text: 'My button' %>Namely, using a method other than <%= component :section, "Some Label" do %>
Some content.
<% end %> |
This comment has been minimized.
This comment has been minimized.
|
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:
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.
One of the advantages to the current approach of passing a class name ( More broadly, whether we use the existing 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. |
This comment has been minimized.
This comment has been minimized.
@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 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 |
This comment has been minimized.
This comment has been minimized.
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. |
27eb8ef
to
8ff1390
This comment has been minimized.
This comment has been minimized.
|
@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 —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 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 |
This comment has been minimized.
This comment has been minimized.
|
I also kinda like the idea of a flat structure in If components each had their own directories, would the class definition of the component look something like |
8ff1390
to
ccc1bf8
This comment has been minimized.
This comment has been minimized.
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 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"? |
This comment has been minimized.
This comment has been minimized.
|
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(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
endIt could also potentially encapsulate some of the testing behavior from inferring the def test_render_component
assert_equal(
%(<span title="my title">Hello, World!</span>),
TestComponent[title: "my title"] { "Hello, World!" }.to_html
)
endThough stylistically this gets a bit towards more of what one would see in a PORO-oriented design, just a few ideas and musings. |
This comment has been minimized.
This comment has been minimized.
I like that
Given a component class such as 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. |
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
jaredcwhite
commented
Jun 9, 2019
|
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. |
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
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
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 solvedIn 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:
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: :internalThen, during assets building, it will check all the routes starting with 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. |
This comment has been minimized.
This comment has been minimized.
|
I assume that we'd want to support a |
This comment has been minimized.
This comment has been minimized.
paxer
commented
Jun 11, 2019
•
|
this is a great proposal as a starting point, what I would personally like to see in so
|
This comment has been minimized.
This comment has been minimized.
ravicious
commented
Jun 11, 2019
|
Just FYI regarding the presentational/container component approach by Dan Abramov, the linked blogpost starts with a disclaimer:
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. |
This comment has been minimized.
This comment has been minimized.
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) |
This comment has been minimized.
This comment has been minimized.
OmriSama
commented
Jun 13, 2019
|
I know that ReactRails/ReactOnRails do something similar where you define components that are rendered on the server through Ruby functions in the controller, but how cool would it be if this Component interface allowed you to specify React components? That way I could write React UI and access the view via a Prop or some sort of Context hook. Whoo!! I'm excited |
This comment has been minimized.
This comment has been minimized.
|
@morgoth I've updated the PR body to more explicitly communicate what the merged changes enable |
This comment has been minimized.
This comment has been minimized.
I think switching components sounds like the right call in this case.
So far, we've avoided having one component inherit from another, with the exception of inheriting from |
This comment has been minimized.
This comment has been minimized.
Simplifying template lookup is a definite benefit here. We're currently using
Right now, we're organizing our app-specific components into folders based on the model they render. So in
And then for our design system components, we have the following in
Generally, components from @nogtini I feel like this also touches on your comment as well, FWIW. @ravicious I agree with your perspective on the distinctions about types of components. This is something we are still wrestling with. |
This comment has been minimized.
This comment has been minimized.
Honestly, we went through so many different possible interfaces as we worked on this internally. In the end, sticking with Our hope is that whatever we end up with will be friendly to engineers picking this pattern up for the first time. |
This comment has been minimized.
This comment has been minimized.
In our experience, inference of views has gotten us into trouble, especially when it comes to optimizing view performance. This is why we've stuck with direct class name references: they are unambiguous. |
This comment has been minimized.
This comment has been minimized.
|
@rmacklin @jaredcwhite @dubadub @paxer @viniciusoyama How asset management might benefit from a corresponding level of encapsulation has definitely crossed our minds. We have some ideas about how we might experiment with this when it comes to our design system components especially. Our current plan is to continue to build out components internally and see where the asset management pain points reveal themselves. (I'm confident they will soon enough |
This comment has been minimized.
This comment has been minimized.
Correct. We currently have an |
This comment has been minimized.
This comment has been minimized.
Today I've been working on moving our components to use sidecar template files, with the idea that this is something we'll want to provide out of the box. |
This comment has been minimized.
This comment has been minimized.
This is precisely why we're working to get view components into Rails. We've made it a priority internally to be as close to stock Rails as possible, which makes it difficult to argue for using something like |
This comment has been minimized.
This comment has been minimized.
We've struggled with this debate internally. While embedding other syntax in Ruby (I could see arguments for ERB/Slim/HAML/etc, CSS, JS, and others) might be appealing, we've run into friction/awkwardness with the inline The reality is that many editors struggle to highlight multiple syntaxes in the same file. Heck, we don't reliably highlight HEREDOCs in diffs here on GitHub! |
This comment has been minimized.
This comment has been minimized.
Are we 100% sure? Not at all. As you have said, there is plenty of evidence in the community that people are finding success with this strategy. A primary focus for us as we continue to work on this project is how to reason about, and then communicate, the role view components should fill in the architecture of a Rails app. For now, we are focusing on building out our component architecture so that we can learn more through experience. Our latest "ah-ha!" moment has been that perhaps view components have the potential to be a codification of view models/presenters/decorators, common patterns in many Rails apps but not part of the official Rails architecture today. To that end, we're experimenting with creating components that are combinations of existing view & view model pairs. |
This comment has been minimized.
This comment has been minimized.
Oh, how I would love to open-source our components so you could see how much this is already true for us! Our component stack has very few HTML tags at this point. Most of our components are using |
This comment has been minimized.
This comment has been minimized.
I'm so glad to feel your excitement, too! |
This comment has been minimized.
This comment has been minimized.
domchristie
commented
Jun 14, 2019
|
I'm also excited about this, and love how elegant the solution is! Just wondered how this might work with Russian doll caching? In particular would the caching system need to be updated in order to invalidate when the internals of |
This comment has been minimized.
This comment has been minimized.
rip747
commented
Jun 14, 2019
|
These feel exactly like ViewModels from ASP.Net |
This comment has been minimized.
This comment has been minimized.
|
Not the same thing, but this reminds me of components back in the Rails 1.x days: https://github.com/rails/rails/blob/1-2-stable/actionpack/lib/action_controller/components.rb :) |
This comment has been minimized.
This comment has been minimized.
sunnyrjuneja
commented
Jun 17, 2019
|
quick question, what's the release target for this? 6.0 or 6.1? |
This comment has been minimized.
This comment has been minimized.
|
Rails 6 has shipped a release candidate already, we won't start that over just to add this. So this is 6.1. Please note: this, as is, adds support for rendering objects that respond to |
- Note that Rails 6.1 has support for `render_in` but doesn't actually add ActionView::Component (confused me for a while and explained by rails/rails#36388 (comment))
This comment has been minimized.
This comment has been minimized.
joshleblanc
commented
Jul 16, 2019
•
|
Question re: sidecar templates. If you're already using sidecar templates, it would make sense to have a sidecar scss file. If such were the case, would it make sense to make the scss locally scoped to the component? I'm thinking of svelte here, where a component has it's own script/css/html and it's all locally scoped. Except in this case, they would be separate files. So styling for one component could have
and it wouldn't affect another component's div. |
This comment has been minimized.
This comment has been minimized.
Spone
commented
Jul 16, 2019
|
@HorizonShadow yes it does totally make sense. That's the convention we use when working with Komponent, but it is not enforced by the gem. |
joelhawksley commentedJun 3, 2019
•
edited
Introduce support for ActionView::Component
This PR introduces structural support for
ActionView::Component, a framework for building view components.Specifically, it modifies
ActionView::RenderingHelper#renderto support passing in an object torenderthatresponds_torender_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
ActionView::Componentat 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 3700 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.
ActionView::Component
ActionView::Componentis an effort to address these pain points in a way that improves the Rails view layer.Building Components
Components are subclasses of
ActionView::Componentand live inapp/components.Components implement a
.templateclass method with a HEREDOC containing ERB, and an optional initializer. All other methods should be private.Components support ActiveModel validations. Components are validated after initialization, but before rendering.
Example
Given the component
app/components/test_component.rb:We can render it in a view as:
Which returns:
Error case
If the component is rendered with a blank title:
An error will be raised:
ActiveModel::ValidationError: Validation failed: Title can't be blankTesting
Components are unit tested directly. The
render_componenttest helper renders a component and wraps the result inNokogiri.HTML, allowing us to test the component above as:In general, we’ve found it makes the most sense to test components based on their rendered HTML, as their only public instance method is
html.Benefits
Testing
ActionView::Componentallows views to be unit-tested. Our unit tests run in around 25ms/test, vs. ~6s/test for integration tests.Code Coverage
ActionView::Componentis 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.
Performance
In early benchmarks, we’ve seen performance improvements over the existing rendering pipeline. For a test page with nested renders 10 levels deep, we’re seeing around a 5x increase in speed over partials:
Rails 6.1.0.alpha, joelhawksley/actionview-component-demo, /benchmark route, via
RAILS_ENV=production rails s, measured with evanphx/benchmark-ipsOpen Questions
API
We’ve been pretty happy with what we have so far.
@cgriego and others have suggested that we consider using a hash key (like
render(component: TestComponent)), but in practice this has felt overly verbose, especially in cases where we might be rendering combinations of dozens of components.@zachahn and others have suggested we pass an instantiated component to #render which feels a little more Ruby-like, but a little less Railsy to me.
Rails integration
We’re not sure if the current integration point in Rails (
ActionView::RenderingHelper#render) is the best option.Naming
Traditionally, Rails views have been named according to the controller actions that render them. With
ActionView::Component, things aren’t so simple.We’ve seen good results mimicking the levels of abstraction Dan Abramov calls Presentational and Container Components. Presentational components worry about how things look, and Container components worry about how things work.
For Presentational components, we’ve been naming them according to the UI element they represent (
Tag,Badge, etc).For Container components, we’ve been namespacing them within the domain concept they represent (
Languages,Topics,PullRequests) and naming them according to the Presentational component they build (Tag,Badge, etc).Template architecture
Having the ERB template inside a Ruby file doesn’t feel quite right.
Not all editors like highlighting HEREDOCs. Perhaps we could allow a sidecar template file instead? (i.e.
component.html.erbsitting alongsidecomponent.rb)@skyksandr mentioned that Sinatra does inline templates at the bottom of the file.
Currently, we use HEREDOCs with string interpolation disabled. This doesn’t feel super user-friendly, and weird errors arise in certain situations if string interpolation is enabled.
Templating engine support
We should probably support arbitrary templating engines like normal Rails views.
Use of ActiveModel::Validations
As @dhh pointed out when we discussed this at RailsConf, we’re using
ActiveModel::Validationsin a non-user-facing manner, something not traditionally done.Component previews
@xdmx and others have suggested we consider something akin to
ActionMailer::Previewto render components individually.(Not) Calling
superininitializeWe are not calling
superin our component'sinitializemethods as the currentActionView::Baseinitializer is built for view files. This awkward inheritance is a symptom ofActionView::Componentbeing a subset of the functionality inActionView::Base. @tenderlove believes we should eventually flip the inheritance structure so thatActionView::Basecontains only what is need for both regular views and components.Support for other Rails niceties
We’d probably want to support url helpers, generators, locales, request formats, and other existing Rails view layer features. Right now helpers must be explicitly included before they can be used in a component.
Existing implementations
ActionView::Componentis 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
ActionView::Component, including but not limited to: @cgriego, @xdmx, @skyksandr, @jcoyne, @dylanahsmith, @kennyadsl, @cquinones100, @erikaxel, @zachahn, and @trcarden.