Skip to content

Commit

Permalink
Override ViewComponent::Base#sidecar_files instead of the compiler.
Browse files Browse the repository at this point in the history
  • Loading branch information
cbeer committed Sep 27, 2024
1 parent 023c1b0 commit c68be30
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 75 deletions.
58 changes: 12 additions & 46 deletions lib/blacklight/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,23 @@
module Blacklight
class Component < ViewComponent::Base
class << self
# Workaround for https://github.com/ViewComponent/view_component/issues/1565
def config
@config ||= ViewComponent::Config.defaults.merge(ViewComponent::Base.config)
end
alias upstream_sidecar_files sidecar_files
alias upstream_compiler compiler

# rubocop:disable Naming/MemoizedInstanceVariableName
def compiler
@__vc_compiler ||= EngineCompiler.new(self)
end
# rubocop:enable Naming/MemoizedInstanceVariableName

alias sidecar_files _sidecar_files unless ViewComponent::Base.respond_to? :sidecar_files
end
@__vc_compiler = nil unless Rails.env.production?

EXCLUDE_VARIABLES = [
:@lookup_context, :@view_renderer, :@view_flow, :@view_context,
:@tag_builder, :@current_template,
:@__vc_set_slots, :@__vc_original_view_context,
:@__vc_variant, :@__vc_content_evaluated,
:@__vc_render_in_block, :@__vc_content, :@__vc_helpers
].freeze

def inspect
# Exclude variables added by render_in
render_variables = instance_variables - EXCLUDE_VARIABLES
fields = render_variables.map { |ivar| "#{ivar}:#{instance_variable_get(ivar).inspect}" }.join(', ')
"#<#{self.class.name}:#{object_id} #{fields}>"
end

class EngineCompiler < ::ViewComponent::Compiler
# ViewComponent::Compiler locates and caches templates from sidecar files to the component source file.
# While this is sensible in a Rails application, it prevents component templates defined in an Engine
# from being overridden by an installing application without subclassing the component, which may also
# require modifying any partials rendering the component. This subclass of compiler overrides the template
# location algorithm to take the sidecar file names from the Engine, but look to see if a file of the
# same name existing in the installing application (ie, under Rails.root). If the latter exists, this
# compiler will cache that template instead of the engine-defined file; if not, the compiler will fall
# back to the engine-defined file.
def templates
@templates ||= begin
extensions = ActionView::Template.template_handler_extensions
upstream_compiler
end

component_class.sidecar_files(extensions).each_with_object([]) do |path, memo|
pieces = File.basename(path).split(".")
app_path = Rails.root.join(path.slice(path.index(component_class.view_component_path)..-1).to_s).to_s
def sidecar_files(*args, **kwargs)
upstream_sidecar_files(*args, **kwargs).map do |path|
app_path = Rails.root.join(path.slice(path.index(view_component_path)..-1).to_s).to_s

memo << {
path: File.exist?(app_path) ? app_path : path,
variant: pieces.second.split("+").second&.to_sym,
handler: pieces.last
}
if File.exist?(app_path)
app_path
else
path
end
end
end
Expand Down
53 changes: 24 additions & 29 deletions spec/lib/blacklight/component_spec.rb
Original file line number Diff line number Diff line change
@@ -1,43 +1,38 @@
# frozen_string_literal: true

RSpec.describe Blacklight::Component do
let(:component_class) { Blacklight::DocumentTitleComponent }
RSpec.describe Blacklight::Component, type: :component do
let(:component_class) { Blacklight::System::ModalComponent }

context "subclassed" do
it "returns our Compiler implementation" do
expect(component_class.ancestors).to include described_class
expect(component_class.compiler).to be_a Blacklight::Component::EngineCompiler
before do
ViewComponent::CompileCache.invalidate!

component_class.class_eval do
undef :call if method_defined?(:call)
end
end

describe Blacklight::Component::EngineCompiler do
subject(:compiler) { described_class.new(component_class) }

let(:original_compiler) { ViewComponent::Compiler.new(component_class) }
let(:original_path) { original_compiler.send(:templates).first[:path] }
let(:resolved_path) { compiler.templates.first[:path] }

context "without overrides" do
it "links to engine template" do
expect(resolved_path).not_to include(".internal_test_app")
expect(resolved_path).to eql(original_path)
end
context "without overrides" do
it "renders the engine template" do
render_inline(component_class.new)
expect(page).to have_css('.modal-header')
end
end

context "with overrides" do
let(:path_match) do
Regexp.new(Regexp.escape(File.join(".internal_test_app", component_class.view_component_path)))
context "with overrides" do
around do |ex|
FileUtils.mkdir_p(Rails.root.join('app/components/blacklight/system'))
Rails.root.join("app/components/blacklight/system/modal_component.html.erb").open("w") do |f|
f.puts '<div class="custom-modal">Overridden</div>'
end

before do
allow(File).to receive(:exist?).and_call_original
allow(File).to receive(:exist?).with(path_match).and_return(true)
end
ex.run
ensure
Rails.root.join('app/components/blacklight/system/modal_component.html.erb').unlink
end

it "links to application template" do
expect(resolved_path).to include(".internal_test_app")
expect(resolved_path).not_to eql(original_path)
end
it "renders to application template" do
render_inline(component_class.new)
expect(page).to have_css('.custom-modal')
end
end
end

0 comments on commit c68be30

Please sign in to comment.