Skip to content

Commit eda4782

Browse files
authored
Merge pull request #501 from github/bmw/compiler
Extract template compiler from base
2 parents 88a6a0c + 27a0f87 commit eda4782

File tree

4 files changed

+224
-173
lines changed

4 files changed

+224
-173
lines changed

lib/view_component.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module ViewComponent
66
extend ActiveSupport::Autoload
77

88
autoload :Base
9+
autoload :Compiler
910
autoload :Preview
1011
autoload :PreviewTemplateError
1112
autoload :TestHelpers

lib/view_component/base.rb

Lines changed: 6 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def render_in(view_context, &block)
7777
before_render
7878

7979
if render?
80-
send(self.class.call_method_name(@variant))
80+
render_template_for(@variant)
8181
else
8282
""
8383
end
@@ -208,80 +208,20 @@ def inherited(child)
208208
super
209209
end
210210

211-
def call_method_name(variant)
212-
if variant.present? && variants.include?(variant)
213-
"call_#{variant}"
214-
else
215-
"call"
216-
end
217-
end
218-
219211
def compiled?
220-
CompileCache.compiled?(self)
212+
template_compiler.compiled?
221213
end
222214

223215
# Compile templates to instance methods, assuming they haven't been compiled already.
224216
#
225217
# Do as much work as possible in this step, as doing so reduces the amount
226218
# of work done each time a component is rendered.
227219
def compile(raise_errors: false)
228-
return if compiled?
229-
230-
if template_errors.present?
231-
raise ViewComponent::TemplateError.new(template_errors) if raise_errors
232-
return false
233-
end
234-
235-
if instance_methods(false).include?(:before_render_check)
236-
ActiveSupport::Deprecation.warn(
237-
"`before_render_check` will be removed in v3.0.0. Use `before_render` instead."
238-
)
239-
end
240-
241-
# Remove any existing singleton methods,
242-
# as Ruby warns when redefining a method.
243-
remove_possible_singleton_method(:variants)
244-
remove_possible_singleton_method(:collection_parameter)
245-
remove_possible_singleton_method(:collection_counter_parameter)
246-
remove_possible_singleton_method(:counter_argument_present?)
247-
248-
define_singleton_method(:variants) do
249-
templates.map { |template| template[:variant] } + variants_from_inline_calls(inline_calls)
250-
end
251-
252-
define_singleton_method(:collection_parameter) do
253-
if provided_collection_parameter
254-
provided_collection_parameter
255-
else
256-
name.demodulize.underscore.chomp("_component").to_sym
257-
end
258-
end
259-
260-
define_singleton_method(:collection_counter_parameter) do
261-
"#{collection_parameter}_counter".to_sym
262-
end
263-
264-
define_singleton_method(:counter_argument_present?) do
265-
instance_method(:initialize).parameters.map(&:second).include?(collection_counter_parameter)
266-
end
267-
268-
validate_collection_parameter! if raise_errors
269-
270-
templates.each do |template|
271-
# Remove existing compiled template methods,
272-
# as Ruby warns when redefining a method.
273-
method_name = call_method_name(template[:variant])
274-
undef_method(method_name.to_sym) if instance_methods.include?(method_name.to_sym)
275-
276-
class_eval <<-RUBY, template[:path], -1
277-
def #{method_name}
278-
@output_buffer = ActionView::OutputBuffer.new
279-
#{compiled_template(template[:path])}
280-
end
281-
RUBY
282-
end
220+
template_compiler.compile(raise_errors: raise_errors)
221+
end
283222

284-
CompileCache.register self
223+
def template_compiler
224+
@_template_compiler ||= Compiler.new(self)
285225
end
286226

287227
# we'll eventually want to update this to support other types
@@ -346,111 +286,6 @@ def provided_collection_parameter
346286
@provided_collection_parameter ||= nil
347287
end
348288

349-
def compiled_template(file_path)
350-
handler = ActionView::Template.handler_for_extension(File.extname(file_path).gsub(".", ""))
351-
template = File.read(file_path)
352-
353-
if handler.method(:call).parameters.length > 1
354-
handler.call(self, template)
355-
else
356-
handler.call(OpenStruct.new(source: template, identifier: identifier, type: type))
357-
end
358-
end
359-
360-
def inline_calls
361-
@inline_calls ||=
362-
begin
363-
# Fetch only ViewComponent ancestor classes to limit the scope of
364-
# finding inline calls
365-
view_component_ancestors =
366-
ancestors.take_while { |ancestor| ancestor != ViewComponent::Base } - included_modules
367-
368-
view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq
369-
end
370-
end
371-
372-
def inline_calls_defined_on_self
373-
@inline_calls_defined_on_self ||= instance_methods(false).grep(/^call/)
374-
end
375-
376-
def matching_views_in_source_location
377-
return [] unless source_location
378-
379-
location_without_extension = source_location.chomp(File.extname(source_location))
380-
381-
extensions = ActionView::Template.template_handler_extensions.join(",")
382-
383-
# view files in the same directory as the component
384-
sidecar_files = Dir["#{location_without_extension}.*{#{extensions}}"]
385-
386-
# view files in a directory named like the component
387-
directory = File.dirname(source_location)
388-
filename = File.basename(source_location, ".rb")
389-
component_name = name.demodulize.underscore
390-
391-
sidecar_directory_files = Dir["#{directory}/#{component_name}/#{filename}.*{#{extensions}}"]
392-
393-
(sidecar_files - [source_location] + sidecar_directory_files)
394-
end
395-
396-
def templates
397-
@templates ||=
398-
matching_views_in_source_location.each_with_object([]) do |path, memo|
399-
pieces = File.basename(path).split(".")
400-
401-
memo << {
402-
path: path,
403-
variant: pieces.second.split("+").second&.to_sym,
404-
handler: pieces.last
405-
}
406-
end
407-
end
408-
409-
def template_errors
410-
@template_errors ||=
411-
begin
412-
errors = []
413-
414-
if (templates + inline_calls).empty?
415-
errors << "Could not find a template file or inline render method for #{self}."
416-
end
417-
418-
if templates.count { |template| template[:variant].nil? } > 1
419-
errors << "More than one template found for #{self}. There can only be one default template file per component."
420-
end
421-
422-
invalid_variants = templates
423-
.group_by { |template| template[:variant] }
424-
.map { |variant, grouped| variant if grouped.length > 1 }
425-
.compact
426-
.sort
427-
428-
unless invalid_variants.empty?
429-
errors << "More than one template found for #{'variant'.pluralize(invalid_variants.count)} #{invalid_variants.map { |v| "'#{v}'" }.to_sentence} in #{self}. There can only be one template file per variant."
430-
end
431-
432-
if templates.find { |template| template[:variant].nil? } && inline_calls_defined_on_self.include?(:call)
433-
errors << "Template file and inline render method found for #{self}. There can only be a template file or inline render method per component."
434-
end
435-
436-
duplicate_template_file_and_inline_variant_calls =
437-
templates.pluck(:variant) & variants_from_inline_calls(inline_calls_defined_on_self)
438-
439-
unless duplicate_template_file_and_inline_variant_calls.empty?
440-
count = duplicate_template_file_and_inline_variant_calls.count
441-
442-
errors << "Template #{'file'.pluralize(count)} and inline render #{'method'.pluralize(count)} found for #{'variant'.pluralize(count)} #{duplicate_template_file_and_inline_variant_calls.map { |v| "'#{v}'" }.to_sentence} in #{self}. There can only be a template file or inline render method per variant."
443-
end
444-
445-
errors
446-
end
447-
end
448-
449-
def variants_from_inline_calls(calls)
450-
calls.reject { |call| call == :call }.map do |variant_call|
451-
variant_call.to_s.sub("call_", "").to_sym
452-
end
453-
end
454289
end
455290

456291
ActiveSupport.run_load_hooks(:view_component, self)

0 commit comments

Comments
 (0)