view_component-form 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +16 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +170 -0
  5. data/app/components/view_component/form/base_component.rb +53 -0
  6. data/app/components/view_component/form/button_component.rb +19 -0
  7. data/app/components/view_component/form/check_box_component.rb +27 -0
  8. data/app/components/view_component/form/collection_check_boxes_component.rb +50 -0
  9. data/app/components/view_component/form/collection_radio_buttons_component.rb +50 -0
  10. data/app/components/view_component/form/collection_select_component.rb +49 -0
  11. data/app/components/view_component/form/color_field_component.rb +9 -0
  12. data/app/components/view_component/form/date_field_component.rb +9 -0
  13. data/app/components/view_component/form/date_select_component.rb +34 -0
  14. data/app/components/view_component/form/datetime_field_component.rb +9 -0
  15. data/app/components/view_component/form/datetime_local_field_component.rb +9 -0
  16. data/app/components/view_component/form/datetime_select_component.rb +34 -0
  17. data/app/components/view_component/form/email_field_component.rb +9 -0
  18. data/app/components/view_component/form/field_component.rb +65 -0
  19. data/app/components/view_component/form/file_field_component.rb +9 -0
  20. data/app/components/view_component/form/grouped_collection_select_component.rb +57 -0
  21. data/app/components/view_component/form/label_component.rb +42 -0
  22. data/app/components/view_component/form/month_field_component.rb +9 -0
  23. data/app/components/view_component/form/number_field_component.rb +9 -0
  24. data/app/components/view_component/form/password_field_component.rb +9 -0
  25. data/app/components/view_component/form/radio_button_component.rb +27 -0
  26. data/app/components/view_component/form/range_field_component.rb +9 -0
  27. data/app/components/view_component/form/rich_text_area_component.rb +9 -0
  28. data/app/components/view_component/form/search_field_component.rb +9 -0
  29. data/app/components/view_component/form/select_component.rb +37 -0
  30. data/app/components/view_component/form/submit_component.rb +19 -0
  31. data/app/components/view_component/form/telephone_field_component.rb +9 -0
  32. data/app/components/view_component/form/text_area_component.rb +9 -0
  33. data/app/components/view_component/form/text_field_component.rb +9 -0
  34. data/app/components/view_component/form/time_field_component.rb +9 -0
  35. data/app/components/view_component/form/time_select_component.rb +34 -0
  36. data/app/components/view_component/form/time_zone_select_component.rb +36 -0
  37. data/app/components/view_component/form/url_field_component.rb +9 -0
  38. data/app/components/view_component/form/week_field_component.rb +9 -0
  39. data/lib/generators/vcf/builder/builder_generator.rb +32 -0
  40. data/lib/generators/vcf/builder/templates/builder.rb.erb +4 -0
  41. data/lib/view_component/form/builder.rb +212 -0
  42. data/lib/view_component/form/class_names_helper.rb +37 -0
  43. data/lib/view_component/form/engine.rb +15 -0
  44. data/lib/view_component/form/test_helpers.rb +29 -0
  45. data/lib/view_component/form/version.rb +7 -0
  46. data/lib/view_component/form.rb +5 -0
  47. metadata +152 -0
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Form
5
+ class TelephoneFieldComponent < FieldComponent
6
+ self.tag_klass = ActionView::Helpers::Tags::TelField
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Form
5
+ class TextAreaComponent < FieldComponent
6
+ self.tag_klass = ActionView::Helpers::Tags::TextArea
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Form
5
+ class TextFieldComponent < FieldComponent
6
+ self.tag_klass = ActionView::Helpers::Tags::TextField
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Form
5
+ class TimeFieldComponent < FieldComponent
6
+ self.tag_klass = ActionView::Helpers::Tags::TimeField
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Form
5
+ class TimeSelectComponent < FieldComponent
6
+ attr_reader :html_options
7
+
8
+ def initialize(form, object_name, method_name, options = {}, html_options = {})
9
+ @html_options = html_options
10
+
11
+ super(form, object_name, method_name, options)
12
+
13
+ set_html_options!
14
+ end
15
+
16
+ def call
17
+ ActionView::Helpers::Tags::TimeSelect.new(
18
+ object_name,
19
+ method_name,
20
+ @view_context,
21
+ options,
22
+ html_options
23
+ ).render
24
+ end
25
+
26
+ protected
27
+
28
+ def set_html_options!
29
+ @html_options[:class] = class_names(html_options[:class], html_class)
30
+ @html_options.delete(:class) if @html_options[:class].blank?
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Form
5
+ class TimeZoneSelectComponent < FieldComponent
6
+ attr_reader :priority_zones, :html_options
7
+
8
+ def initialize(form, object_name, method_name, priority_zones, options = {}, html_options = {}) # rubocop:disable Metrics/ParameterLists
9
+ @priority_zones = priority_zones
10
+ @html_options = html_options
11
+
12
+ super(form, object_name, method_name, options)
13
+
14
+ set_html_options!
15
+ end
16
+
17
+ def call
18
+ ActionView::Helpers::Tags::TimeZoneSelect.new(
19
+ object_name,
20
+ method_name,
21
+ @view_context,
22
+ priority_zones,
23
+ options,
24
+ html_options
25
+ ).render
26
+ end
27
+
28
+ protected
29
+
30
+ def set_html_options!
31
+ @html_options[:class] = class_names(html_options[:class], html_class)
32
+ @html_options.delete(:class) if @html_options[:class].blank?
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Form
5
+ class UrlFieldComponent < FieldComponent
6
+ self.tag_klass = ActionView::Helpers::Tags::UrlField
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewComponent
4
+ module Form
5
+ class WeekFieldComponent < FieldComponent
6
+ self.tag_klass = ActionView::Helpers::Tags::WeekField
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Vcf
6
+ module Generators
7
+ class BuilderGenerator < Rails::Generators::NamedBase
8
+ source_root File.join(File.dirname(__FILE__), "templates")
9
+
10
+ class_option :namespace, default: "Form"
11
+ class_option :path, default: "lib"
12
+
13
+ def create_builder_from_template
14
+ template "builder.rb.erb", destination
15
+ end
16
+
17
+ protected
18
+
19
+ def class_name
20
+ name.camelize
21
+ end
22
+
23
+ def components_namespace
24
+ options[:namespace].camelize
25
+ end
26
+
27
+ def destination
28
+ "#{options[:path]}/#{name.underscore}.rb"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ class <%= class_name %> < ViewComponent::Form::Builder
2
+ # Set the namespace you want to use for your own components
3
+ namespace "<%= components_namespace %>"
4
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view"
4
+
5
+ module ViewComponent
6
+ module Form
7
+ class Builder < ActionView::Helpers::FormBuilder
8
+ class Error < StandardError; end
9
+
10
+ class NotImplementedComponentError < Error; end
11
+
12
+ class NamespaceAlreadyAddedError < Error; end
13
+
14
+ class_attribute :lookup_namespaces, default: [ViewComponent::Form]
15
+
16
+ class << self
17
+ def inherited(base)
18
+ base.lookup_namespaces = lookup_namespaces.dup
19
+
20
+ super
21
+ end
22
+
23
+ def namespace(namespace)
24
+ if lookup_namespaces.include?(namespace)
25
+ raise NamespaceAlreadyAddedError, "The component namespace '#{namespace}' is already added"
26
+ end
27
+
28
+ lookup_namespaces.prepend namespace
29
+ end
30
+ end
31
+
32
+ def initialize(*)
33
+ @__component_klass_cache = {}
34
+
35
+ super
36
+ end
37
+
38
+ (field_helpers - %i[label check_box radio_button fields_for fields hidden_field file_field]).each do |selector|
39
+ class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
40
+ def #{selector}(method, options = {}) # def text_field(method, options = {})
41
+ render_component( # render_component(
42
+ :#{selector}, # :text_field,
43
+ @object_name, # @object_name,
44
+ method, # method,
45
+ objectify_options(options), # objectify_options(options),
46
+ ) # )
47
+ end # end
48
+ RUBY_EVAL
49
+ end
50
+
51
+ # See: https://github.com/rails/rails/blob/33d60cb02dcac26d037332410eabaeeb0bdc384c/actionview/lib/action_view/helpers/form_helper.rb#L2280
52
+ def label(method, text = nil, options = {}, &block)
53
+ render_component(:label, @object_name, method, text, objectify_options(options), &block)
54
+ end
55
+
56
+ def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
57
+ render_component(
58
+ :check_box, @object_name, method, checked_value, unchecked_value, objectify_options(options)
59
+ )
60
+ end
61
+
62
+ def radio_button(method, tag_value, options = {})
63
+ render_component(
64
+ :radio_button, @object_name, method, tag_value, objectify_options(options)
65
+ )
66
+ end
67
+
68
+ def file_field(method, options = {})
69
+ self.multipart = true
70
+ render_component(:file_field, @object_name, method, objectify_options(options))
71
+ end
72
+
73
+ def submit(value = nil, options = {})
74
+ if value.is_a?(Hash)
75
+ options = value
76
+ value = nil
77
+ end
78
+ value ||= submit_default_value
79
+ render_component(:submit, value, options)
80
+ end
81
+
82
+ def button(value = nil, options = {}, &block)
83
+ if value.is_a?(Hash)
84
+ options = value
85
+ value = nil
86
+ end
87
+ value ||= submit_default_value
88
+ render_component(:button, value, options, &block)
89
+ end
90
+
91
+ # SELECTORS.each do |selector|
92
+ # class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
93
+ # def #{selector}(*args)
94
+ # render_component(
95
+ # :#{selector},
96
+ # *args,
97
+ # super,
98
+ # )
99
+ # end
100
+ # RUBY_EVAL
101
+ # end
102
+
103
+ # See: https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionview/lib/action_view/helpers/form_options_helper.rb
104
+ def select(method, choices = nil, options = {}, html_options = {}, &block)
105
+ render_component(
106
+ :select, @object_name, method, choices, objectify_options(options),
107
+ @default_html_options.merge(html_options), &block
108
+ )
109
+ end
110
+
111
+ # rubocop:disable Metrics/ParameterLists
112
+ def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
113
+ render_component(
114
+ :collection_select, @object_name, method, collection, value_method, text_method,
115
+ objectify_options(options), @default_html_options.merge(html_options)
116
+ )
117
+ end
118
+
119
+ def grouped_collection_select(
120
+ method, collection,
121
+ group_method, group_label_method, option_key_method, option_value_method,
122
+ options = {}, html_options = {}
123
+ )
124
+ render_component(
125
+ :grouped_collection_select, @object_name, method, collection, group_method,
126
+ group_label_method, option_key_method, option_value_method,
127
+ objectify_options(options), @default_html_options.merge(html_options)
128
+ )
129
+ end
130
+
131
+ def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
132
+ render_component(
133
+ :collection_check_boxes, @object_name, method, collection, value_method, text_method,
134
+ objectify_options(options), @default_html_options.merge(html_options), &block
135
+ )
136
+ end
137
+
138
+ def collection_radio_buttons(
139
+ method, collection,
140
+ value_method, text_method,
141
+ options = {}, html_options = {},
142
+ &block
143
+ )
144
+ render_component(
145
+ :collection_radio_buttons, @object_name, method, collection, value_method, text_method,
146
+ objectify_options(options), @default_html_options.merge(html_options), &block
147
+ )
148
+ end
149
+ # rubocop:enable Metrics/ParameterLists
150
+
151
+ def date_select(method, options = {}, html_options = {})
152
+ render_component(
153
+ :date_select, @object_name, method,
154
+ objectify_options(options), @default_html_options.merge(html_options)
155
+ )
156
+ end
157
+
158
+ def datetime_select(method, options = {}, html_options = {})
159
+ render_component(
160
+ :datetime_select, @object_name, method,
161
+ objectify_options(options), @default_html_options.merge(html_options)
162
+ )
163
+ end
164
+
165
+ def time_select(method, options = {}, html_options = {})
166
+ render_component(
167
+ :time_select, @object_name, method,
168
+ objectify_options(options), @default_html_options.merge(html_options)
169
+ )
170
+ end
171
+
172
+ def time_zone_select(method, options = {}, html_options = {})
173
+ render_component(
174
+ :time_zone_select, @object_name, method,
175
+ objectify_options(options), @default_html_options.merge(html_options)
176
+ )
177
+ end
178
+
179
+ if defined?(ActionView::Helpers::Tags::ActionText)
180
+ def rich_text_area(method, options = {})
181
+ render_component(:rich_text_area, @object_name, method, objectify_options(options))
182
+ end
183
+ end
184
+
185
+ private
186
+
187
+ def render_component(component_name, *args, &block)
188
+ component = component_klass(component_name).new(self, *args)
189
+ component.render_in(@template, &block)
190
+ end
191
+
192
+ def objectify_options(options)
193
+ @default_options.merge(options.merge(object: @object))
194
+ end
195
+
196
+ def component_klass(component_name)
197
+ @__component_klass_cache[component_name] ||= begin
198
+ component_klass = self.class.lookup_namespaces.filter_map do |namespace|
199
+ "#{namespace}::#{component_name.to_s.camelize}Component".safe_constantize || false
200
+ end.first
201
+
202
+ unless component_klass.is_a?(Class) && component_klass < ViewComponent::Base
203
+ raise NotImplementedComponentError, "Component named #{component_name} doesn't exist" \
204
+ " or is not a ViewComponent::Base class"
205
+ end
206
+
207
+ component_klass
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Backport of https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-class_names
4
+ module ViewComponent
5
+ module Form
6
+ module ClassNamesHelper
7
+ # rubocop:disable Metrics/CyclomaticComplexity
8
+ # rubocop:disable Metrics/MethodLength
9
+ def build_tag_values(*args)
10
+ tag_values = []
11
+
12
+ args.each do |tag_value|
13
+ case tag_value
14
+ when Hash
15
+ tag_value.each do |key, val|
16
+ tag_values << key.to_s if val && key.present?
17
+ end
18
+ when Array
19
+ tag_values.concat build_tag_values(*tag_value)
20
+ else
21
+ tag_values << tag_value.to_s if tag_value.present?
22
+ end
23
+ end
24
+
25
+ tag_values
26
+ end
27
+ # rubocop:enable Metrics/CyclomaticComplexity
28
+ # rubocop:enable Metrics/MethodLength
29
+
30
+ def class_names(*args)
31
+ tokens = build_tag_values(*args).flat_map { |value| value.to_s.split(/\s+/) }.uniq
32
+
33
+ safe_join(tokens, " ")
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+
5
+ module ViewComponent
6
+ module Form
7
+ # :nodoc:
8
+ class Engine < ::Rails::Engine
9
+ config.autoload_once_paths = %W[
10
+ #{root}/app/components
11
+ #{root}/app/lib
12
+ ]
13
+ end
14
+ end
15
+ end