A more sane way of rendering view components from Erb, HAML, etc. #1293
Replies: 5 comments 2 replies
-
Hey @bradgessler, thanks for opening this discussion! Syntax changes have been suggested before in #1204 and #385. My answer to both would be that this most likely won't be supported officially as it adds to the helper namespace. However, the linked discussions demonstrate that there's demand for something like this, so your changes may be welcome to users of ViewComponent. You might have some luck proposing this feature to view_component-contrib. |
Beta Was this translation helpful? Give feedback.
-
Nice concept, even if not accepted its a great snippet and I'm using it. Would be nice if a component generated and registered its own helper (given I've not looked closely at the workings of rails helpers) from a potential efficiency perspective. |
Beta Was this translation helpful? Give feedback.
-
@paul-ridgway @bradgessler what if we did |
Beta Was this translation helpful? Give feedback.
-
My preference would definitely be for With the module ApplicationComponentHelper
def simple_form_for(*args, **opts, &)
opts[:builder] ||= SimpleForm::Tailwind::FormBuilder
super(*args, **opts, &)
end
private
def method_missing(method_name, *args, **opts, &)
super unless match_component_helper?(method_name)
capture do
render ComponentFinder.new(method_name, self).call(*args, **opts), &
end
end
def respond_to_missing?(method_name, include_private = false)
match_component_helper?(method_name) || super
end
def match_component_helper?(method_name)
# ends in _component or _collection
method_name =~ /\w+_(component|collection)$/
end
end ComponentFinder = Struct.new(:name, :scope) do
def call(...)
return component.with_collection(...) if name.end_with?("_collection")
component.new(...)
end
private
def component
regular_component || shared_component || no_component_error
end
def regular_component
[parent_module, unscoped_name, "Component"].flatten.join("::").safe_constantize
end
def shared_component
["Shared", unscoped_name, "Component"].flatten.join("::").safe_constantize
end
def parent_module
# When the helper method is called in a view or partial, the scope
# will be ActionView::Base.
scope.is_a?(ViewComponent::Base) ? scope.class.module_parent : nil
end
def unscoped_name
name.to_s.remove(/_(component|collection)$/).camelize
end
def method
name.end_with?("_collection") ? :with_collection : :new
end
def no_component_error
raise "`#{unscoped_name}::Component` is not defined in this scope (#{parent_module || 'ActionView::Base'})."
end
end |
Beta Was this translation helpful? Give feedback.
-
Another option is using your own registry, which I prefer over using method missing: module ComponentHelpers
HELPERS = {
card: "CardComponent",
label: "LabelComponent",
badge: "BadgeComponent"
}
HELPERS.each do |name, component|
define_method "#{name}_component" do |*args, **kwargs, &block|
render component.constantize.new(*args, **kwargs), &block
end
end
end
class BaseComponent < ViewComponent::Base
include ComponentHelpers
end
class UserCard < BaseComponent
def call
card_component do |card|
card.title { "Hello" }
end
end
end
# OR in .html.erb
<%= badge_component { "Hello" } %> |
Beta Was this translation helpful? Give feedback.
-
If you are tired of calling Rails view component like this:
Start calling them like this:
I can open a PR with the helper at https://gist.github.com/bradgessler/cd91d5f2e333b99eed2fc0c3f76d827b in the lib if the maintainers of this repo would be interested in merging it in.
Beta Was this translation helpful? Give feedback.
All reactions