lib/action_view/component.rb in actionview-component-0.1.0 vs lib/action_view/component.rb in actionview-component-1.0.0
- old
+ new
@@ -1,23 +1,132 @@
-module ActionView
- class Component < Base
+# frozen_string_literal: true
+
+# Monkey patch ActionView::Base#render to support ActionView::Component
+#
+# Upstreamed in https://github.com/rails/rails/pull/36388
+# Necessary for Rails versions < 6.1.0.alpha
+class ActionView::Base
+ module RenderMonkeyPatch
+ def render(component, _ = nil, &block)
+ return super unless component.respond_to?(:render_in)
+
+ component.render_in(self, &block)
+ end
end
- class TemplateRenderer
- module ComponentTemplates
- def render(context, options)
- if options.key?(:component)
- name = options[:component]
- klass = "#{name}_component".classify.constantize
- args = options.except(:component)
- context = args.empty? ? klass.new : klass.new(args)
+ prepend RenderMonkeyPatch unless Rails::VERSION::MINOR > 0 && Rails::VERSION::MAJOR == 6
+end
- super context, template: "components/#{name}"
+module ActionView
+ class Component < ActionView::Base
+ VERSION = "1.0.0"
+
+ include ActiveModel::Validations
+
+ # Entrypoint for rendering components. Called by ActionView::Base#render.
+ #
+ # view_context: ActionView context from calling view
+ # args(hash): params to be passed to component being rendered
+ # block: optional block to be captured within the view context
+ #
+ # returns HTML that has been escaped by the respective template handler
+ #
+ # Example subclass:
+ #
+ # app/components/my_component.rb:
+ # class MyComponent < ActionView::Component
+ # def initialize(title:)
+ # @title = title
+ # end
+ # end
+ #
+ # app/components/my_component.html.erb
+ # <span title="<%= @title %>">Hello, <%= content %>!</span>
+ #
+ # In use:
+ # <%= render MyComponent.new(title: "greeting") do %>world<% end %>
+ # returns:
+ # <span title="greeting">Hello, world!</span>
+ #
+ def render_in(view_context, *args, &block)
+ self.class.compile
+ @content = view_context.capture(&block) if block_given?
+ validate!
+ call
+ end
+
+ def initialize(*); end
+
+ class << self
+ def inherited(child)
+ child.include Rails.application.routes.url_helpers unless child < Rails.application.routes.url_helpers
+
+ super
+ end
+
+ # Compile template to #call instance method, assuming it hasn't been compiled already.
+ # We could in theory do this on app boot, at least in production environments.
+ # Right now this just compiles the template the first time the component is rendered.
+ def compile
+ return if @compiled
+
+ class_eval("def call; @output_buffer = ActionView::OutputBuffer.new; #{compiled_template}; end")
+
+ @compiled = true
+ end
+
+ private
+
+ def compiled_template
+ handler = ActionView::Template.handler_for_extension(File.extname(template_file_path).gsub(".", ""))
+ template = File.read(template_file_path)
+
+ if handler.method(:call).parameters.length > 1
+ handler.call(DummyTemplate.new, template)
else
- super
+ handler.call(DummyTemplate.new(template))
end
end
+
+ def template_file_path
+ raise NotImplementedError.new("#{self} must implement #initialize.") unless self.instance_method(:initialize).owner == self
+
+ filename = self.instance_method(:initialize).source_location[0]
+ filename_without_extension = filename[0..-(File.extname(filename).length + 1)]
+ sibling_files = Dir["#{filename_without_extension}.*"] - [filename]
+
+ if sibling_files.length > 1
+ raise StandardError.new("More than one template found for #{self}. There can only be one sidecar template file per component.")
+ end
+
+ if sibling_files.length == 0
+ raise NotImplementedError.new(
+ "Could not find a template for #{self}. Either define a .template method or add a sidecar template file."
+ )
+ end
+
+ sibling_files[0]
+ end
end
- prepend ComponentTemplates
+ class DummyTemplate
+ attr_reader :source
+
+ def initialize(source = nil)
+ @source = source
+ end
+
+ def identifier
+ ""
+ end
+
+ # we'll eventually want to update this to support other types
+ def type
+ "text/html"
+ end
+ end
+
+ private
+
+ attr_reader :content
end
end