From 3ec138123911f7b4eebe13afe3643583d85c1bc5 Mon Sep 17 00:00:00 2001 From: Joel Warrington Date: Fri, 24 May 2024 17:39:36 -0600 Subject: [PATCH] Add lookup_chain customizability --- CHANGELOG.md | 1 + README.md | 23 ++++++++++++++-- lib/view_component/form/configuration.rb | 9 ++++++- lib/view_component/form/renderer.rb | 6 ++--- .../app/components/form/text_field.rb | 9 +++++++ spec/view_component/form/builder_spec.rb | 21 +++++++++++++++ .../view_component/form/configuration_spec.rb | 26 +++++++++++++++++++ 7 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 spec/internal/app/components/form/text_field.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index e52f38f5..9a1b3d44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added parent_component configuration for field components (#160) - Added Ruby 3.3 support (#164) +- Add `lookup_chain` customizability (#162) ### Removed - Drop Ruby 2.7 support (#164) diff --git a/README.md b/README.md index 37a9697f..028964a1 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ Development of this gem is sponsored by: ## Compatibility -> [!WARNING] -> **This is an early release, and the API is subject to change until `v1.0.0`.** +> [!WARNING] > **This is an early release, and the API is subject to change until `v1.0.0`.** This gem is tested on: @@ -36,6 +35,26 @@ end | --------------------------- | ----------------------------------------------------- | ----------------------- | | `parent_component` (string) | Parent class for all `ViewComponent::Form` components | `"ViewComponent::Base"` | +#### Configuring component lookup + +`ViewComponent::Form` will automatically infer the component class with a `Component` suffix. You can customize the lookup using the `lookup_chain`: + +```rb +# config/initializers/vcf.rb + +ViewComponent::Form.configure do |config| + without_component_suffix = lambda do |component_name, namespaces: []| + namespaces.lazy.map do |namespace| + "#{namespace}::#{component_name.to_s.camelize}".safe_constantize + end.find(&:itself) + end + + config.lookup_chain.prepend(without_component_suffix) +end +``` + +`ViewComponent::Form` will iterate through the `lookup_chain` until a value is returned. By using `prepend` we can fallback on the default `ViewComponent::Form` lookup. + ## Usage Add your own form builder. diff --git a/lib/view_component/form/configuration.rb b/lib/view_component/form/configuration.rb index 9f9d0084..a05fb7b2 100644 --- a/lib/view_component/form/configuration.rb +++ b/lib/view_component/form/configuration.rb @@ -3,10 +3,17 @@ module ViewComponent module Form class Configuration - attr_accessor :parent_component + attr_accessor :parent_component, :lookup_chain def initialize @parent_component = "ViewComponent::Base" + @lookup_chain = [ + lambda do |component_name, namespaces: []| + namespaces.lazy.map do |namespace| + "#{namespace}::#{component_name.to_s.camelize}Component".safe_constantize + end.find(&:itself) + end + ] end end end diff --git a/lib/view_component/form/renderer.rb b/lib/view_component/form/renderer.rb index 5dc93268..060b5114 100644 --- a/lib/view_component/form/renderer.rb +++ b/lib/view_component/form/renderer.rb @@ -52,9 +52,9 @@ def objectify_options(options) def component_klass(component_name) @__component_klass_cache[component_name] ||= begin - component_klass = self.class.lookup_namespaces.filter_map do |namespace| - "#{namespace}::#{component_name.to_s.camelize}Component".safe_constantize || false - end.first + component_klass = ViewComponent::Form.configuration.lookup_chain.lazy.map do |lookup| + lookup.call(component_name, namespaces: lookup_namespaces) + end.find(&:itself) unless component_klass.is_a?(Class) && component_klass < ViewComponent::Base raise NotImplementedComponentError, "Component named #{component_name} doesn't exist " \ diff --git a/spec/internal/app/components/form/text_field.rb b/spec/internal/app/components/form/text_field.rb new file mode 100644 index 00000000..953a19dc --- /dev/null +++ b/spec/internal/app/components/form/text_field.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Form + class TextField < ViewComponent::Form::LabelComponent + def call + "my custom text_field" + end + end +end diff --git a/spec/view_component/form/builder_spec.rb b/spec/view_component/form/builder_spec.rb index ee8e8d82..a5a6e965 100644 --- a/spec/view_component/form/builder_spec.rb +++ b/spec/view_component/form/builder_spec.rb @@ -170,6 +170,27 @@ it { expect(builder.send(:component_klass, :text_field)).to eq(Form::TextFieldComponent) } it { expect(builder.send(:component_klass, :submit)).to eq(ViewComponent::Form::SubmitComponent) } end + + context "with a custom lookup_chain" do + let(:builder) { CustomFormBuilder.new(object_name, object, template, options) } + + around do |example| + original = ViewComponent::Form.configuration.lookup_chain + ViewComponent::Form.configuration.lookup_chain.prepend(lambda do |component_name, namespaces: []| + namespaces.lazy.map do |namespace| + "#{namespace}::#{component_name.to_s.camelize}".safe_constantize + end.find(&:itself) + end) + + example.run + + ViewComponent::Form.configuration.lookup_chain = original + end + + it { expect(builder.send(:component_klass, :label)).to eq(Form::LabelComponent) } + it { expect(builder.send(:component_klass, :text_field)).to eq(Form::TextField) } + it { expect(builder.send(:component_klass, :submit)).to eq(ViewComponent::Form::SubmitComponent) } + end end describe "#field_id" do diff --git a/spec/view_component/form/configuration_spec.rb b/spec/view_component/form/configuration_spec.rb index fb4b0387..cbb7066d 100644 --- a/spec/view_component/form/configuration_spec.rb +++ b/spec/view_component/form/configuration_spec.rb @@ -7,5 +7,31 @@ it do expect(configuration).to have_attributes(parent_component: "ViewComponent::Base") end + + describe "#lookup_chain" do + subject(:lookup_chain) { described_class.new.lookup_chain } + + it "by default implements one lookup lambda" do + expect(lookup_chain.length).to be(1) + end + + it "uses Component suffix" do + expect( + lookup_chain.first.call(:text_field, namespaces: [ViewComponent::Form]) + ).to be(ViewComponent::Form::TextFieldComponent) + end + + it "finds the first klass that exists when given a list of namespaces" do # rubocop:disable RSpec/ExampleLength + expect( + lookup_chain.first.call( + :text_field, + namespaces: [ + Form, + ViewComponent::Form + ] + ) + ).to be(Form::TextFieldComponent) + end + end end end