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

Feature/multiple templates arguments #1096

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jsolas
Copy link

@jsolas jsolas commented Oct 13, 2021

Summary
Fixes #387

This PR builds on the branch by @joelhawksley, @fermion, @fsateler and adds the ability to specify arguments for the generated functions, based on what was discussed #387 (comment). In this PR:

1- Each template would define a method called render_<template_name>(locals = {})
2- We will not be defining the locals inside the generated method like rails does. They would need to be accessed with locals[:foo]

The logic being that one may want to use templates to extract sections, without wanting to build a full component. For example, I could have a Table component, and I would want to have the row definition in a sidecar template. This requires being able to pass in each item for every iteration:

class TableComponent < ViewComponent::Base
  def initialize(models)
    @models = models
  end
end
<%# table_component.html.erb %>
<table>
  <% @models.each do |model| %>
    <%= render_row model: model %>
  <% end %>
</table>
<%# row.html.erb %>
<tr>
  <%# I can use `model` here %>
  <td><%= locals[:model].name  %></td>
</tr>

Co-authored-by: Rob Sterner <fermion@github.com>
@jsolas jsolas requested a review from a team as a code owner October 13, 2021 17:50
@jsolas jsolas force-pushed the feature/multiple-templates-arguments branch 2 times, most recently from 86b4392 to 666913c Compare October 13, 2021 19:06
Copy link
Contributor

@fsateler fsateler left a comment

Choose a reason for hiding this comment

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

Thanks for picking this up @jsolas! I've added a few comments.

adr/0003-multiple-templates-per-component.md Show resolved Hide resolved
adr/0003-multiple-templates-per-component.md Outdated Show resolved Hide resolved

The generated methods are only invokable via keyword arguments

The generated methods cannot have arguments with default values.
Copy link
Contributor

Choose a reason for hiding this comment

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

No, but they can be simulated by the view.


The generated methods cannot have arguments with default values.

The generated methods are public, and thus could be invoked by a third party.
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be changed, but not sure if it's worth it?

Copy link
Member

Choose a reason for hiding this comment

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

I think it would be nice to introduce them as private to start.

lib/view_component/compiler.rb Outdated Show resolved Hide resolved
test/view_component/view_component_test.rb Outdated Show resolved Hide resolved
@jsolas jsolas force-pushed the feature/multiple-templates-arguments branch 2 times, most recently from fcdace0 to 37dd2fb Compare October 15, 2021 13:19
@jsolas jsolas force-pushed the feature/multiple-templates-arguments branch from 37dd2fb to dff7c0d Compare October 16, 2021 18:43
@jsolas
Copy link
Author

jsolas commented Oct 16, 2021

Hello @BlakeWilliams, @camertron, @joelhawksley, i tried to do what was discussed in #387 (comment), I would like to receive your opinions and see if there is a possibility of moving forward with this. Thanks.

Copy link
Member

@joelhawksley joelhawksley left a comment

Choose a reason for hiding this comment

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

Looking good! I think we're on the right track here so I've marked the ADR as accepted.

@@ -0,0 +1,88 @@
# 1. Allow multiple templates
Copy link
Member

Choose a reason for hiding this comment

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

We need a name for this feature, I think. What about:

Suggested change
# 1. Allow multiple templates
# 3. Template parts


## Status

Proposed.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Proposed.
Accepted

Comment on lines +11 to +13
As components become larger (for example, because you are implementing a whole page), it becomes
useful to be able to extract sections of the view to a different file. ActionView has
partials, and ViewComponent lacks a similar mechanism.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
As components become larger (for example, because you are implementing a whole page), it becomes
useful to be able to extract sections of the view to a different file. ActionView has
partials, and ViewComponent lacks a similar mechanism.
As components become larger (for example, implementing a whole page), it can become useful to be able to extract sections of the view to a different file. ActionView has partials, and ViewComponent lacks a similar mechanism.

Comment on lines +15 to +19
ActionView partials have the problem that their interface is not introspectable. Data
may be passed into the partial via ivars or locals, and it is impossible to know
which without actually opening up the file. Additionally, partials are globally
invocable, thus making it difficult to detect if a given partial is in use or not,
and who are its users.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
ActionView partials have the problem that their interface is not introspectable. Data
may be passed into the partial via ivars or locals, and it is impossible to know
which without actually opening up the file. Additionally, partials are globally
invocable, thus making it difficult to detect if a given partial is in use or not,
and who are its users.
ActionView partials have the problem that their interface is not introspectable. Data may be passed into the partial via ivars or locals, and it is impossible to know which without actually opening up the file. Additionally, partials are globally invocable, thus making it difficult to detect if a given partial is in use or not, and who are its users.

Comment on lines +28 to +29
Allow multiple ERB templates available within the component, and make it possible to
invoke them from the main view.Templates are compiled to methods in the format `render_#{template_basename}(locals = {})`
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Allow multiple ERB templates available within the component, and make it possible to
invoke them from the main view.Templates are compiled to methods in the format `render_#{template_basename}(locals = {})`
Allow multiple ERB templates available within the component, and make it possible to invoke them from the main view. Templates are compiled to methods in the format `render_#{template_basename}(locals = {})`.

@@ -36,6 +36,10 @@ title: Changelog

*Hans Lemuet*

* Add support for multiple templates.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Add support for multiple templates.
* Add support for template parts.

@@ -92,3 +92,36 @@ Component subclasses inherit the parent component's template if they don't defin
class MyLinkComponent < LinkComponent
end
```

## Multiple templates with arguments
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
## Multiple templates with arguments
## Template parts


## Multiple templates with arguments

ViewComponents can render multiple templates defined in the sidecar directory and send arguments to it:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
ViewComponents can render multiple templates defined in the sidecar directory and send arguments to it:
ViewComponents can render multiple templates defined in the sidecar directory:

├── ...
```

Templates are compiled to methods in the format `render_#{template_basename}(locals = {})`, which can then be called in the component.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Templates are compiled to methods in the format `render_#{template_basename}(locals = {})`, which can then be called in the component.
Template parts can then be rendered by calling `render_#{template_basename}`. Template parts accept a single positional argument that is accessible in the template as `locals`, defaulting to an empty hash if no argument is passed.

@@ -331,7 +331,7 @@ def _sidecar_files(extensions)
# view files in the same directory as the component
sidecar_files = Dir["#{directory}/#{component_name}.*{#{extensions}}"]

sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
sidecar_directory_files = Dir["#{directory}/#{component_name}/*.*{#{extensions}}"]
Copy link
Member

Choose a reason for hiding this comment

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

What happens if there is a slot/subcomponent with a template in the Sidecar directory? How can we prevent/manage conflicts between the two?

@BlakeWilliams
Copy link
Contributor

BlakeWilliams commented Oct 21, 2021

Thanks for opening this PR and taking this on!

Similar to how we handled Slots V2, is there a way we could mark this functionality as experimental and have components opt-in to this feature? I think there's a lot of value in trying this functionality out in real applications and making modifications as necessary before making this a stable, long-term API.

@joelhawksley
Copy link
Member

@BlakeWilliams and I were chatting about this PR today and realized that there might be another option we could consider: private slots with templates but no sub-component, where the slot automatically accepts a single argument locals = {}.

For example, we might have:

# table_component.rb
class TableComponent < ViewComponent::Base
  renders_many :rows, private: true

  def initialize(items)
    items.each do |item|
      row({ title: item.title, author: item.author })
    end
  end
end
<!-- table_component.html.erb -->
<% rows.each do |row| %>
  <%= row %>
<% end %>
<!-- table_component/row.html.erb -->
<tr>
  <td><%= locals[:title] %></td>
  <td><%= locals[:author] %></td>
</tr>

I believe this would fulfill the desire for a private API and it would avoid allocating a new component for each row. What do y'all think?

@fsateler
Copy link
Contributor

@joelhawksley What should the rows array contain? An array of (html-safe) strings? Or some other intermediate object? While possibly not allocating a component, it would still store a (possibly) large array of something.

@joelhawksley
Copy link
Member

@fsateler yes, I think you'd have an array of HTML-safe strings.

Given the concerns about performance that are driving this work, I think it might be wise for us to implement a benchmark as part of this PR that compares the number of allocations with the new approach vs. an equivalent case with rendering a component for each row.

@fsateler
Copy link
Contributor

fsateler commented Nov 8, 2021

@joelhawksley I feel this proposal is not very railsy. In particular, the loop in the initializer feels like boilerplate. I don't really like the API.

Moreover, I think this proposal would require to have something like what this PR is proposing and on top adding the private slot API

@joelhawksley
Copy link
Member

@fsateler I think it would also be possible to use the plural slot setter if you didn't want to decompose items, like this:

# table_component.rb
class TableComponent < ViewComponent::Base
  renders_many :rows, private: true

  def initialize(items)
    rows(items)
  end
end

The difference between ☝🏻 and the proposal in this PR is that it remains inside of the existing Slots API, which I think would help reduce potential confusion around its introduction and use.

@joelmoss
Copy link

joelmoss commented Apr 2, 2022

Has this moved along at all recently? I have a real need to be able to dynamically determine the template to render within a component, and this PR seems to enable that. Would be great to get something out, and I would be happy to help.

@joelhawksley
Copy link
Member

@joelmoss I don't think it's moved since my last comment from what I can tell. @jsolas @fsateler would you be OK with @joelmoss taking this on?

@fsateler
Copy link
Contributor

Not a problem on my side! If this helps things move forward, please go ahead!

@fsateler
Copy link
Contributor

Hi! I've been thinking about @joelhawksley 's proposal for a new API. I don't think it's going to work, not without a lot of refactoring:

  1. with_<slotname> creates a SlotV2 object for each item in the collection.
  2. This creation is eager, because at render time we iterate through @_set_slots[slot_name], which must have been prepopulated by with_<slotname>. This means the SlotV2 object creation is eagerly done.

This means we will create an intermediate array of intermediate objects, which is what we wanted to avoid.

Moreover, we still would need a lower-level api for actually rendering the template (which would be used by the slot interface). Please lets move forward with this as the lower-level API 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support multiple views
5 participants