Skip to content

Instantly share code, notes, and snippets.

@nicolas-brousse
Created December 2, 2020 15:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nicolas-brousse/f53835971d3a6e1ac74e96346d2c1aae to your computer and use it in GitHub Desktop.
Save nicolas-brousse/f53835971d3a6e1ac74e96346d2c1aae to your computer and use it in GitHub Desktop.
[WIP] Share of a ViewComponent::FormBuilder
# frozen_string_literal: true
module Form
class BaseComponent < ApplicationComponent
class << self
attr_accessor :default_options
end
include Components::Validations
validates :form, presence: true
validates :object_name, presence: true
delegate :object, to: :form
attr_reader :form, :object_name, :options
def initialize(form, object_name, options = {})
@form = form
@object_name = object_name
@options = options
super()
end
delegate :errors, to: :object, prefix: true
def object_errors?
object.errors.any?
end
def html_class
nil
end
protected
def before_render
super
combine_options!
end
def combine_options!
@options = (self.class.default_options.deep_dup || {}).deep_merge(options).tap do |opts|
opts[:class] = classnames(options[:class], html_class) if (html_class || options[:class]).present?
end
end
end
end
# frozen_string_literal: true
module Form
class FieldComponent < BaseComponent
class << self
attr_accessor :tag_klass
end
validates :method_name, presence: true
attr_reader :method_name
def initialize(form, object_name, method_name, options = {})
@method_name = method_name
super(form, object_name, options)
end
def call
raise "`self.tag_klass' should be defined in #{self.class.name}" unless self.class.tag_klass
self.class.tag_klass.new(object_name, method_name, form, options).render
end
def method_errors
object_errors.full_messages_for(object_method_name)
end
def method_errors?
object_errors.key?(object_method_name)
end
def value
object.public_send(object_method_name)
end
def object_method_name
@object_method_name ||= method_name.to_s.sub(/_id$/, "").to_sym # TODO: try to found a better way to manage _id and _ids
end
end
end
# frozen_string_literal: true
module Form
class TextFieldComponent < FieldComponent
self.tag_klass = Tags::TextField
def html_class
classnames("field", "border-error": method_errors?)
end
end
end
# frozen_string_literal: true
# FormBuilder
module ViewComponent
class FormBuilder < ActionView::Helpers::FormBuilder
class << self
attr_accessor :components_namespace
end
class NotImplementedComponentError < RuntimeError; end
self.components_namespace = "Form"
(field_helpers - %i[label check_box radio_button fields_for fields hidden_field file_field]).each do |selector|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{selector}(method, options = {}) # def text_field(method, options = {})
render_component( # render_component(
:#{selector}, # :text_field,
self, # self,
@object_name, # @object_name,
method, # method,
objectify_options(options), # objectify_options(options),
) # )
end # end
RUBY_EVAL
end
# See: https://github.com/rails/rails/blob/33d60cb02dcac26d037332410eabaeeb0bdc384c/actionview/lib/action_view/helpers/form_helper.rb#L2280
def label(method, text = nil, options = {}, &block)
render_component(:label, self, @object_name, method, text, objectify_options(options), &block)
end
def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
render_component(
:check_box, self, @object_name, method, checked_value, unchecked_value, objectify_options(options)
)
end
# def radio_button(method, tag_value, options = {})
# end
# def file_field(method, options = {})
# end
def submit(value = nil, options = {})
if value.is_a?(Hash)
options = value
value = nil
end
value ||= submit_default_value
render_component(:submit, self, @object_name, value, options)
end
# def button(value = nil, options = {}, &block)
# end
# SELECTORS.each do |selector|
# class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
# def #{selector}(*args)
# render_component(
# :#{selector},
# *args,
# super,
# )
# end
# RUBY_EVAL
# end
# See: https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionview/lib/action_view/helpers/form_options_helper.rb
def select(method, choices = nil, options = {}, html_options = {}, &block)
render_component(
:select, self, @object_name, method, choices, objectify_options(options),
@default_html_options.merge(html_options), &block
)
end
# rubocop:disable Metrics/ParameterLists
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
render_component(
:collection_select, self, @object_name, method, collection, value_method, text_method,
objectify_options(options), @default_html_options.merge(html_options)
)
end
# rubocop:enable Metrics/ParameterLists
# # ---
# IDEA
def field(method, options = {})
# TODO: auto define field from input type
end
def group_field(method, options = {}, &block)
render_component(:group_field, self, @object_name, method, options, &block)
end
# Show field errors
def errors_field(method, options = {})
render_component(:errors_field, self, @object_name, method, options)
end
private
def render_component(component_name, *args, &block)
component_klassname = "#{self.class.components_namespace}::#{component_name.to_s.camelize}Component"
component_klass = component_klassname.safe_constantize
unless component_klass.is_a?(Class) && component_klass < ViewComponent::Base
raise NotImplementedComponentError, "Component #{component_klassname} doesn't exist" \
" or is not a ViewComponent::Base class"
end
component = component_klass.new(*args)
component.render_in(@template, &block)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment