diff --git a/adr/0003-multiple-templates-per-component.md b/adr/0003-multiple-templates-per-component.md new file mode 100644 index 0000000000..29c6edca8e --- /dev/null +++ b/adr/0003-multiple-templates-per-component.md @@ -0,0 +1,73 @@ +# 1. Allow multiple templates + +Date: 2021-10-13 + +## Status + +Proposed. + +## Context + +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. + +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. + +## Considered Options + +* Introduce component partials to components +* Keep components as-is + +### Component partials + +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 = {})` + +**Pros:** +* Better performance due to lack of GC pressure and object creation +* Reduces the number of components needed to express a more complex view. +* Extracted sections are not exposed outside the component, thus reducing component library API surface. + +**Cons:** +* Another concept for users of ViewComponent to learn and understand. +* Components are no longer the only way to encapsulate behavior. + +### Keeping components as-is + +**Pros:** +* The API remains simple and components are the only way to encapsulate behavior. +* Encourages creating reusable sub-components. + +**Cons:** +* Extracting a component results in more GC and intermediate objects. +* Extracting a component may result in tightly coupled but split components. +* Creates new public components thus expanding component library API surface. + +## Decision + +We will allow having multiple templates in the sidecar asset. Each asset will be compiled to +it's own method `render_`. + +## Consequences + +This implementation has better performance characteristics over both an extracted component +and ActionView partials, because it avoids creating intermediate objects, and the overhead of +creating bindings and `instance_exec`. +Having explicit arguments makes the interface explicit. + +TODO: The following are consequences of the current approach, but the approach might be extended +to avoid them: + +The interface to render a sidecar partial would be a method call, and depart from the usual +`render(*)` interface used in ActionView. + +The generated methods are only invokable via keyword arguments + +The generated methods cannot have arguments with default values. + +The generated methods are public, and thus could be invoked by a third party. \ No newline at end of file diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 524aa15072..7105f662bf 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -36,6 +36,10 @@ title: Changelog *Hans Lemuet* +* Add support for multiple templates. + + *Rob Sterner*, *Joel Hawksley* + ## 2.40.0 * Replace antipatterns section in the documentation with best practices. @@ -113,10 +117,6 @@ title: Changelog *Joel Hawksley* -* Add support for multiple templates. - - *Rob Sterner*, *Joel Hawksley* - ## 2.36.0 * Add `slot_type` helper method. diff --git a/docs/guide/templates.md b/docs/guide/templates.md index d55b8ae846..abe693b81d 100644 --- a/docs/guide/templates.md +++ b/docs/guide/templates.md @@ -93,11 +93,11 @@ class MyLinkComponent < LinkComponent end ``` -#### Multiple templates +## Multiple templates with arguments -ViewComponents can render multiple templates defined in the sidecar directory: +ViewComponents can render multiple templates defined in the sidecar directory and send arguments to it: -``` +```console app/components ├── ... ├── test_component.rb @@ -107,7 +107,7 @@ app/components ├── ... ``` -Templates are compiled to methods in the format `call_#{template_basename}`, which can then be called in the component. +Templates are compiled to methods in the format `render_#{template_basename}(locals = {})`, which can then be called in the component. ```ruby class TestComponent < ViewComponent::Base @@ -118,10 +118,10 @@ class TestComponent < ViewComponent::Base def call case @mode when :list - call_list + render_list number: 1 when :summary - call_summary + render_summary string: "foo" end end end -``` \ No newline at end of file +``` diff --git a/lib/view_component/compiler.rb b/lib/view_component/compiler.rb index 5fe6a0200e..4b369bb87f 100644 --- a/lib/view_component/compiler.rb +++ b/lib/view_component/compiler.rb @@ -50,20 +50,22 @@ def compile(raise_errors: false) # set the method name with call_method_name if pieces.first == component_class.name.demodulize.underscore call_method_name(template[:variant]) - # Otherwise, append the name of the template to - # call_method_name + # Otherwise, append the name of the template to render_ else - "#{call_method_name(template[:variant])}_#{pieces.first.to_sym}" + "render_#{pieces.first.to_sym}" end if component_class.instance_methods.include?(method_name.to_sym) component_class.send(:undef_method, method_name.to_sym) end - component_class.class_eval <<-RUBY, template[:path], -1 - def #{method_name} + component_class.class_eval <<-RUBY, template[:path], -2 + def #{method_name}(**locals) + old_buffer = @output_buffer if defined? @output_buffer @output_buffer = ActionView::OutputBuffer.new #{compiled_template(template[:path])} + ensure + @output_buffer = old_buffer end RUBY end diff --git a/test/sandbox/app/components/multiple_templates_component.rb b/test/sandbox/app/components/multiple_templates_component.rb index b42e5a6aa2..62aff04eaf 100644 --- a/test/sandbox/app/components/multiple_templates_component.rb +++ b/test/sandbox/app/components/multiple_templates_component.rb @@ -1,18 +1,7 @@ # frozen_string_literal: true class MultipleTemplatesComponent < ViewComponent::Base - def initialize(mode:) - @mode = mode - + def initialize @items = ["Apple", "Banana", "Pear"] end - - def call - case @mode - when :list - call_list - when :summary - call_summary - end - end end diff --git a/test/sandbox/app/components/multiple_templates_component/list.html.erb b/test/sandbox/app/components/multiple_templates_component/list.html.erb index 02bf02702e..13683cae41 100644 --- a/test/sandbox/app/components/multiple_templates_component/list.html.erb +++ b/test/sandbox/app/components/multiple_templates_component/list.html.erb @@ -1,4 +1,4 @@ -