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

Override ViewComponent::Base#sidecar_files instead of the compiler. #3346

Merged
merged 1 commit into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 10 additions & 47 deletions lib/blacklight/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,20 @@
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

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

alias sidecar_files _sidecar_files unless ViewComponent::Base.respond_to? :sidecar_files
end

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

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
59 changes: 32 additions & 27 deletions spec/lib/blacklight/component_spec.rb
Original file line number Diff line number Diff line change
@@ -1,43 +1,48 @@
# 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
component_class.reset_compiler!
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) }
after do
component_class.reset_compiler!
ViewComponent::CompileCache.invalidate!

let(:original_compiler) { ViewComponent::Compiler.new(component_class) }
let(:original_path) { original_compiler.send(:templates).first[:path] }
let(:resolved_path) { compiler.templates.first[:path] }
component_class.class_eval do
undef :call if method_defined?(:call)
end
end

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