Created
December 2, 2020 15:05
-
-
Save nicolas-brousse/f53835971d3a6e1ac74e96346d2c1aae to your computer and use it in GitHub Desktop.
[WIP] Share of a ViewComponent::FormBuilder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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