From 77ccbac103f519b61797cea7dd7a75789357d7ca Mon Sep 17 00:00:00 2001 From: Felipe Sateler Date: Sat, 22 Aug 2020 19:54:51 -0400 Subject: [PATCH] Add ability to define templates that take parameters ```ruby class TestComponent < ViewComponent::Base template_arguments :list, :multiple # call_list now takes a `multiple` keyword argument def initialize(mode:) @mode = mode end def call case @mode when :list call_list multiple: false when :multilist call_list multiple: true when :summary call_summary end end end ``` --- docs/guide/templates.md | 22 ++++++++ lib/view_component/base.rb | 20 ++++++++ lib/view_component/compiler.rb | 51 +++++++++++++------ .../multiple_templates_component.rb | 14 ++--- .../list.html.erb | 2 +- .../multiple_templates_component.html.erb | 4 ++ .../summary.html.erb | 2 +- test/view_component/base_test.rb | 31 +++++++++++ test/view_component/view_component_test.rb | 8 ++- 9 files changed, 121 insertions(+), 33 deletions(-) create mode 100644 test/sandbox/app/components/multiple_templates_component/multiple_templates_component.html.erb diff --git a/docs/guide/templates.md b/docs/guide/templates.md index 20dcff7923..31ec302663 100644 --- a/docs/guide/templates.md +++ b/docs/guide/templates.md @@ -126,4 +126,26 @@ class TestComponent < ViewComponent::Base end end end +``` + +You can define templates that take parameters, using the `template_arguments` method. + +```ruby +class TestComponent < ViewComponent::Base + template_arguments :list, :multiple # call_list now takes a `multiple` keyword argument + def initialize(mode:) + @mode = mode + end + + def call + case @mode + when :list + call_list multiple: false + when :multilist + call_list multiple: true + when :summary + call_summary + end + end +end ``` \ No newline at end of file diff --git a/lib/view_component/base.rb b/lib/view_component/base.rb index 99695ac7a3..1cfa45beef 100644 --- a/lib/view_component/base.rb +++ b/lib/view_component/base.rb @@ -393,6 +393,26 @@ def compiler @__vc_compiler ||= Compiler.new(self) end + # Contains the arguments for additional templates + # @return [Hash{String => Array}] + # @private + def template_arguments_hash + @template_arguments_hash ||= {} + end + + # Declares arguments that need to be passed to a template. + # + # If a sidecar template needs additional locals that need to be passed + # at call site, then this method should be used. + # + # The signature for the resulting method will use keyword arguments + # + # @param [Symbol, String] template The template that needs arguments + # @param [Array] *args The arguments for the template + def template_arguments(template, *args) + compiler.add_template_arguments(template, *args) + end + # we'll eventually want to update this to support other types # @private def type diff --git a/lib/view_component/compiler.rb b/lib/view_component/compiler.rb index 5fe6a0200e..7cbac6f064 100644 --- a/lib/view_component/compiler.rb +++ b/lib/view_component/compiler.rb @@ -41,29 +41,21 @@ def compile(raise_errors: false) end templates.each do |template| + method_name, method_args = method_name_and_args_for template + # Remove existing compiled template methods, # as Ruby warns when redefining a method. - pieces = File.basename(template[:path]).split(".") - - method_name = - # If the template matches the name of the component, - # 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 - else - "#{call_method_name(template[:variant])}_#{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}(#{method_args.join(", ")}) + 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 @@ -75,10 +67,27 @@ def #{method_name} CompileCache.register(component_class) end + def add_template_arguments(template, *args) + template = template.to_s + raise ArgumentError, "Arguments already defined for template #{template}" if template_arguments.key?(template) + + template_exists = templates.any? { |t| t[:base_name].split(".").first == template } + raise ArgumentError, "Template does not exist: #{template}" unless template_exists + + template_arguments[template] = args + end + private attr_reader :component_class + # Contains the arguments for additional templates + # @return [Hash{String => Array}] + # @private + def template_arguments + @template_arguments ||= {} + end + def define_render_template_for if component_class.instance_methods.include?(:render_template_for) component_class.send(:undef_method, :render_template_for) @@ -199,6 +208,18 @@ def inline_calls end end + def method_name_and_args_for(template) + pieces = template[:base_name].split(".") + if pieces.first == component_class.name.demodulize.underscore + [call_method_name(template[:variant]), []] + else + [ + "#{call_method_name(template[:variant])}_#{pieces.first.to_sym}", + (template_arguments || {}).fetch(pieces.first, []).map { |arg| "#{arg}:" } + ] + end + end + def inline_calls_defined_on_self @inline_calls_defined_on_self ||= component_class.instance_methods(false).grep(/^call/) end diff --git a/test/sandbox/app/components/multiple_templates_component.rb b/test/sandbox/app/components/multiple_templates_component.rb index b42e5a6aa2..96254f0c01 100644 --- a/test/sandbox/app/components/multiple_templates_component.rb +++ b/test/sandbox/app/components/multiple_templates_component.rb @@ -1,18 +1,10 @@ # 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 + template_arguments :list, :number + template_arguments :summary, :string 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..a2d6a3a726 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 @@ -
    +
      <% @items.each do |item| %>
    • <%= item %>
    • <% end %> diff --git a/test/sandbox/app/components/multiple_templates_component/multiple_templates_component.html.erb b/test/sandbox/app/components/multiple_templates_component/multiple_templates_component.html.erb new file mode 100644 index 0000000000..1d18cba1d9 --- /dev/null +++ b/test/sandbox/app/components/multiple_templates_component/multiple_templates_component.html.erb @@ -0,0 +1,4 @@ +
      + <%= call_summary string: "foo" %> + <%= call_list number: 1 %> +
      \ No newline at end of file diff --git a/test/sandbox/app/components/multiple_templates_component/summary.html.erb b/test/sandbox/app/components/multiple_templates_component/summary.html.erb index ef9845aa51..6b988c1bea 100644 --- a/test/sandbox/app/components/multiple_templates_component/summary.html.erb +++ b/test/sandbox/app/components/multiple_templates_component/summary.html.erb @@ -1 +1 @@ -
      The items are: <%= @items.to_sentence %>
      +
      The items are: <%= @items.to_sentence %>, <%= string %>
      diff --git a/test/view_component/base_test.rb b/test/view_component/base_test.rb index ad58006add..2135e87494 100644 --- a/test/view_component/base_test.rb +++ b/test/view_component/base_test.rb @@ -77,4 +77,35 @@ def test_sidecar_files TranslatableComponent._sidecar_files(["yml"]) ) end + + def test_template_arguments_validates_existence + error = assert_raises ArgumentError do + Class.new(ViewComponent::Base) do + def self._sidecar_files(*) + [ + "/Users/fake.user/path/to.templates/component/test_component/test_component.html.erb", + "/Users/fake.user/path/to.templates/component/test_component/sidecar.html.erb", + ] + end + template_arguments :non_existing, [:foo] + end + end + assert_equal "Template does not exist: non_existing", error.message + end + + def test_template_arguments_validates_duplicates + error = assert_raises ArgumentError do + Class.new(ViewComponent::Base) do + def self._sidecar_files(*) + [ + "/Users/fake.user/path/to.templates/component/test_component/test_component.html.erb", + "/Users/fake.user/path/to.templates/component/test_component/sidecar.html.erb", + ] + end + template_arguments :sidecar, [:foo] + template_arguments :sidecar, [:bar] + end + end + assert_equal "Arguments already defined for template sidecar", error.message + end end diff --git a/test/view_component/view_component_test.rb b/test/view_component/view_component_test.rb index 571b5fcc23..7595103efc 100644 --- a/test/view_component/view_component_test.rb +++ b/test/view_component/view_component_test.rb @@ -749,15 +749,13 @@ def test_collection_component_with_trailing_comma_attr_reader end def test_render_multiple_templates - render_inline(MultipleTemplatesComponent.new(mode: :list)) + render_inline(MultipleTemplatesComponent.new) + assert_selector("div", text: "The items are: Apple, Banana, and Pear, foo") assert_selector("li", text: "Apple") assert_selector("li", text: "Banana") assert_selector("li", text: "Pear") - - render_inline(MultipleTemplatesComponent.new(mode: :summary)) - - assert_selector("div", text: "Apple, Banana, and Pear") + assert_selector("div.container") end def test_renders_component_using_rails_config