For us, the winning combination has been ViewComponents, TailwindCSS, and Hotwire.
Out of curiosity, how do you manage standard forms? Do you define a component for each input like TextInputComponent, DateInputComponent, TextAreaComponent, etc.?
For us, the winning combination has been ViewComponents, TailwindCSS, and Hotwire.
Out of curiosity, how do you manage standard forms? Do you define a component for each input like TextInputComponent, DateInputComponent, TextAreaComponent, etc.?
That is a rather bold statement, and I beg to differ. You can, not necessarily will, make your view dependencies unmanageable. You can do so with ViewComponents just as well. There is literally nothing stopping you from making a mess of components, just as with of views/partials. Itās a question of experience, not technology. The rhetorics youāve used is pretty much like the one that ServiceObject pattern adepts are using to bash Rails in general. Tsk-tsk-tsk.
Approach one takes have little to no effect on eventual mess-iness one ends up with. Whether this is an honest mistake, an ignorance, or intentional disinformation, your statement is misleading, at best. Blatantly ā¦bad, at worst.
I didnāt advocate for ViewComponent. I didnāt say it was a solution to dependency management.
When you combine 2 facts
Then the only way to figure out your view dependencies must be by looking through the entirety of nested views, partials (also helpers), see what you call where, how you construct it, and write it all down. This is what I mean by unmanageable.
ViewComponents donāt solve it entirely, because you can still have a deep tree of objects all calling into various models.
My advice was to have a single object that represents 100% of what you will be rendering at this endpoint, sans html. All values needed for this view are in one place.
You made a harsh characterization, but didnāt provide any counterargument. How exactly would you keep view dependencies manageable with vanilla templating approach and without gathering all view dependencies in one place?
My apologies, I did indeed missed the part where you were proposing a more monolithic approach. With portrayal
being an example implementation, if I understand rightly.
Looking at that portrayal
gem of yours I canāt shake the feeling that itās an, essentially, Yet Another Decorator Pattern with but a few bells and whistles on the side.
Youāve mentioned ādependencies ⦠scattered across many filesā as a negative effect of status quo ā yet,
Yes, the data access is performed via one proxy object, but:
@decorator.person.address.street
instead of @person.address.street
, really.One interesting (un)related analogy comes to mind: There are 13 US states in which Tesla can not sell their cars directly, as a manufacturer, because local laws demand that all car sales be made via dealerships and dealerships only. Which results in, roughly, 10% increase in car price for an end customer. Who gets the benefit in all this, one may ask? Why, the proxies, of course ā the parasites that make enough money to buy and lobby this state of things for almost a hundred years now. Direct sales are better in everything, and, most importantly, in the end cost. Yet, there are still people who invent 1000 and 1 reasons to justify their existence and their right to make money off you just because they can. And forbid you from getting rid of them, too.
Luckily for us, nobody here is forcing anything on anyone. But the reasons for your approach seems just as elusiveā¦
Now, despite this looking like mere rant, Iām genuinely interested in how the positive aspects, if any (am still not convinced), overweight the added complexity and, whatās worse, boilerplate code (latter being the same one best Rails is fighting against). After all, if someone went through all the effort to put it all up and together, there must be a good reason for that, innit?
Sure, the reason this becomes different is because you will build an object like this, based on your example:
class MyPage < ApplicationStruct
keyword :address do
keyword :street
keyword :city
end
end
And you will have to construct it somehow. So you will probably add a constructor like this:
def self.from_user(user)
new address: Address.new(street: user.address_street, city: user.address_city)
end
In controller you would then write:
@page = MyPage.from_user(current_user)
And in views everything will be done via this object:
<strong>street:</strong> <%= @page.address.street %>
This way you know everything that went into creating the āpageā. You see a list of every value it requires (declared as keywords), and you see how a user
model turns into those keywords (thanks to the from_user
constructor).
Moreover, these objects can be reused for JSON and HTML, if your 2 APIs mostly match. Crucially, these objects shouldnāt expose models, they should directly serve the needs of the front-end. That means only primitive (json-serializable) types (strings, numerics, arrays, POROs with the same types). I wrote an article on how to structure these objects.
When your dependencies are this clear, you can optimize this page. For example, you can cache the entire MyPage
object, knowing exactly the basis for cache invalidation (because dependencies are clear). You can collect all data for MyPage
with a single SQL query ā because you have a single constructor where all data is set. You can construct MyPage
in tests and conveniently test it. You can open rails console, run MyPage.from_user(User.last)
and see exactly what the payload looks like for any particular user, without the html. In my experience, this has been worth it.
Just my two pfennigs here: this is a compelling example. Really like this idea.
Walter
I feel like the only thing I miss when trying to use partials as templates is being able to yield into multiple sections. I wonder if the Rails team would be more open to baking something like nice_partials into the framework
nice_partials
seems really promising, even more if it gets added to the Rails core.
The advantage that I see over ViewComponents is that you donāt have to create an extra file with Ruby code, even for simple things.
The downside is that you canāt have helper functions inside the component and that you donāt have previews.
I noticed that nice_partials doesnāt support collections / arrays. ViewComponent is better for that:
Iām not sure if I got you correctly, but I think this is currently possible if you render a partial as a layout.
render(layout: "path/to/partial", locals: locals, &blk)
Since you pass the block, if you call <%= yield %>
from the partial, you will be able to have wrapped erb.
For example, you can make a helper like:
def render_my_partial(**kwargs, &blk)
render(layout: 'path/to/partial', locals: kwargs, &blk)
end
And then use it like this:
<%= render_my_partial(pass: 'locals') do %>
<div>some content to be used inside the partial</div>
<% end %>
and if you yield with an argument, you could switch on that argument:
<%= render_my_partial do |some_arg| %>
<% if some_arg == 'section_a' %>
<div>content for section_a</div>
<% else %>
<div>content for section_b</div>
<% end %>
<% end %>
Hereās an example of what I meant:
Ah I think I understand. Not sure about ViewComponents, but I believe you can build a similar feature with the layout+block approach, if you yield a āconfig-likeā object on which you can call link
or any other methods, then use them.
The wrapping partial would look like this:
<% config = Config.new; yield(config) %>
<%# now you can use stuff that block added to the config %>
<% config.links.each do |link| %>
<%= link_to "ā¦" %>
<% end %>
and outside this wouldāve been exactly as the issue showed
<%= render_my_partial do |config| %>
<% config.link(ā¦) %>
<% end %>
But I agree it would be nice to have something more elegant built-in for this.
I would like to share the solution that we finally used:
https://answers.abstractbrain.com/using-rails-helpers-x-component-for-rendering-viewcomponents
Basically we use ViewComponents, but with an improved, compact syntax for rendering.
Donāt understand what you guys mean.
From what I understand, ERB partials already supported what you have tried to accomplish.
__nav.html.erb
<nav><% render_content.call %></nav>
page.html.erb
<%= render partial: 'nav', locals: {
render_content: lambda do %>
<%= link_to "Link 1", page_path %>
<%= link_to "Link 2", another_path %>
<% end } %>
I used it in lots of projects since, like, five years ago; am I missing something?