shoelace-rails 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/app/helpers/shoelace/components/error_wrappable.rb +21 -0
  4. data/app/helpers/shoelace/components/sl_checkbox.rb +40 -0
  5. data/app/helpers/shoelace/components/sl_collection_radio_buttons.rb +47 -0
  6. data/app/helpers/shoelace/components/sl_color_picker.rb +30 -0
  7. data/app/helpers/shoelace/components/sl_input.rb +33 -0
  8. data/app/helpers/shoelace/components/sl_radio_button.rb +20 -0
  9. data/app/helpers/shoelace/components/sl_range.rb +22 -0
  10. data/app/helpers/shoelace/components/sl_select.rb +77 -0
  11. data/app/helpers/shoelace/components/sl_switch.rb +23 -0
  12. data/app/helpers/shoelace/components/sl_textarea.rb +20 -0
  13. data/app/helpers/shoelace/form_builder.rb +8 -0
  14. data/app/helpers/shoelace/sl_form_builder.rb +111 -0
  15. data/app/helpers/shoelace/sl_form_helper.rb +139 -0
  16. data/lib/shoelace/rails/version.rb +1 -1
  17. data/lib/shoelace/railtie.rb +2 -4
  18. data/test/helpers/form_builder/sl_checkbox_test.rb +45 -0
  19. data/test/helpers/form_builder/sl_color_picker_test.rb +26 -0
  20. data/test/helpers/form_builder/sl_input_test.rb +126 -0
  21. data/test/helpers/form_builder/sl_radio_group_test.rb +63 -0
  22. data/test/helpers/form_builder/sl_range_test.rb +34 -0
  23. data/test/helpers/form_builder/sl_select_test.rb +320 -0
  24. data/test/helpers/form_builder/sl_switch_test.rb +26 -0
  25. data/test/helpers/form_builder/sl_textarea_test.rb +72 -0
  26. data/test/helpers/form_builder_test.rb +16 -0
  27. data/test/helpers/form_helper_test.rb +3 -442
  28. data/test/helpers/translation_test.rb +2 -4
  29. data/test/test_helper.rb +22 -0
  30. metadata +25 -4
  31. data/app/helpers/shoelace/form_helper.rb +0 -408
@@ -1,408 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Shoelace
4
- module FormHelper
5
- class ShoelaceInputField < ActionView::Helpers::Tags::TextField #:nodoc:
6
- attr_reader :field_type
7
-
8
- def initialize(field_type, *args)
9
- super(*args)
10
- @field_type = field_type
11
- end
12
-
13
- def render(&block)
14
- options = @options.stringify_keys
15
-
16
- value = options.fetch("value") { value_before_type_cast }
17
- options["value"] = value if value.present?
18
-
19
- options["size"] = options["maxlength"] unless options.key?("size")
20
- options["type"] ||= field_type
21
- options["class"] ||= [options["class"], Shoelace.invalid_input_class_name].compact.join(" ") if @object.respond_to?(:errors) && @object.errors[@method_name].present?
22
-
23
- add_default_name_and_id(options)
24
-
25
- @template_object.content_tag('sl-input', '', options, &block)
26
- end
27
- end
28
-
29
- class ShoelaceColorPicker < ActionView::Helpers::Tags::ColorField #:nodoc:
30
- RGB_VALUE_REGEX = /#[0-9a-fA-F]{6}/
31
-
32
- def field_type; nil; end
33
-
34
- def tag(tag_name, *args, &block)
35
- tag_name.to_s == 'input' ? content_tag('sl-color-picker', '', *args, &block) : super
36
- end
37
-
38
- private
39
-
40
- def validate_color_string(string)
41
- string.downcase if RGB_VALUE_REGEX.match?(string)
42
- end
43
- end
44
-
45
- class ShoelaceRange < ActionView::Helpers::Tags::NumberField #:nodoc:
46
- def field_type; nil; end
47
-
48
- def tag(tag_name, *args, &block)
49
- tag_name.to_s == 'input' ? content_tag('sl-range', '', *args, &block) : super
50
- end
51
- end
52
-
53
- class ShoelaceSwitch < ActionView::Helpers::Tags::TextField #:nodoc:
54
- def field_type; nil; end
55
-
56
- def render(&block)
57
- options = @options.stringify_keys
58
- options["value"] = options.fetch("value") { value_before_type_cast }
59
- add_default_name_and_id(options)
60
- label = options.delete('label').presence || @method_name.to_s.humanize
61
-
62
- @template_object.content_tag('sl-switch', label, options, &block)
63
- end
64
- end
65
-
66
- class ShoelaceTextArea < ActionView::Helpers::Tags::TextArea #:nodoc:
67
- def render(&block)
68
- options = @options.stringify_keys
69
- options["value"] = options.fetch("value") { value_before_type_cast }
70
- add_default_name_and_id(options)
71
-
72
- @template_object.content_tag("sl-textarea", '', options, &block)
73
- end
74
- end
75
-
76
- class ShoelaceSelect < ActionView::Helpers::Tags::Select #:nodoc:
77
- def grouped_options_for_select(grouped_options, options)
78
- @template_object.grouped_sl_options_for_select(grouped_options, options)
79
- end
80
-
81
- def options_for_select(container, options = nil)
82
- @template_object.sl_options_for_select(container, options)
83
- end
84
-
85
- def select_content_tag(option_tags, _options, html_options)
86
- html_options = html_options.stringify_keys
87
- html_options['value'] ||= value
88
- add_default_name_and_id(html_options)
89
-
90
- @template_object.content_tag("sl-select", option_tags, html_options)
91
- end
92
- end
93
-
94
- class ShoelaceCollectionSelect < ActionView::Helpers::Tags::CollectionSelect #:nodoc:
95
- def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
96
- @template_object.sl_options_from_collection_for_select(collection, value_method, text_method, selected)
97
- end
98
-
99
- def select_content_tag(option_tags, _options, html_options)
100
- html_options = html_options.stringify_keys
101
- html_options['value'] ||= value
102
- add_default_name_and_id(html_options)
103
-
104
- @template_object.content_tag("sl-select", option_tags, html_options)
105
- end
106
- end
107
-
108
- class ShoelaceGroupedCollectionSelect < ActionView::Helpers::Tags::GroupedCollectionSelect #:nodoc:
109
- def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
110
- @template_object.sl_option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key)
111
- end
112
-
113
- def select_content_tag(option_tags, _options, html_options)
114
- html_options = html_options.stringify_keys
115
- html_options['value'] ||= value
116
- add_default_name_and_id(html_options)
117
-
118
- @template_object.content_tag("sl-select", option_tags, html_options)
119
- end
120
- end
121
-
122
- class ShoelaceCheckBox < ActionView::Helpers::Tags::CheckBox #:nodoc:
123
- def render(&block)
124
- options = @options.stringify_keys
125
- options["value"] = @checked_value
126
- options["checked"] = true if input_checked?(options)
127
- label = options.delete("label") || @method_name.to_s.humanize
128
-
129
- if options["multiple"]
130
- add_default_name_and_id_for_value(@checked_value, options)
131
- options.delete("multiple")
132
- else
133
- add_default_name_and_id(options)
134
- end
135
-
136
- include_hidden = options.delete("include_hidden") { true }
137
-
138
- sl_checkbox_tag = if block_given?
139
- @template_object.content_tag('sl-checkbox', '', options, &block)
140
- else
141
- @template_object.content_tag('sl-checkbox', label || @method_name.to_s.humanize, options)
142
- end
143
-
144
- if include_hidden
145
- hidden_field_for_checkbox(options) + sl_checkbox_tag
146
- else
147
- sl_checkbox_tag
148
- end
149
- end
150
- end
151
-
152
- class ShoelaceRadioButton < ActionView::Helpers::Tags::RadioButton #:nodoc:
153
- def render(&block)
154
- options = @options.stringify_keys
155
- options["value"] = @tag_value
156
- add_default_name_and_id_for_value(@tag_value, options)
157
- options.delete("name")
158
-
159
- @template_object.content_tag('sl-radio', '', options.except("type"), &block)
160
- end
161
- end
162
-
163
- class ShoelaceCollectionRadioButtons < ActionView::Helpers::Tags::CollectionRadioButtons #:nodoc:
164
- class RadioButtonBuilder < Builder # :nodoc:
165
- def label(*)
166
- text
167
- end
168
-
169
- def radio_button(extra_html_options = {}, &block)
170
- html_options = extra_html_options.merge(@input_html_options)
171
- html_options[:skip_default_ids] = false
172
- @template_object.sl_radio_button(@object_name, @method_name, @value, html_options, &block)
173
- end
174
- end
175
-
176
- def render(&block)
177
- render_collection_for(RadioButtonBuilder, &block)
178
- end
179
-
180
- private
181
-
182
- def render_collection(&block)
183
- html_options = @html_options.stringify_keys
184
- html_options["value"] = value
185
- add_default_name_and_id(html_options)
186
- html_options["label"] = @options[:label].presence || @method_name.to_s.humanize
187
-
188
- @template_object.content_tag('sl-radio-group', html_options) { super(&block) }
189
- end
190
-
191
- def hidden_field
192
- ''.html_safe
193
- end
194
-
195
- def render_component(builder)
196
- builder.radio_button { builder.label }
197
- end
198
- end
199
-
200
- class ShoelaceFormBuilder < ActionView::Helpers::FormBuilder #:nodoc:
201
- {
202
- email: :email,
203
- number: :number,
204
- password: :password,
205
- search: :search,
206
- telephone: :tel,
207
- phone: :tel,
208
- text: :text,
209
- url: :url
210
- }.each do |field_type, field_class|
211
- # def email_field(method, **options, &block)
212
- # ShoelaceInputField.new(:email, object_name, method, @template, options.with_defaults(label: label_text(method))).render(&block)
213
- # end
214
- eval <<-RUBY, nil, __FILE__, __LINE__ + 1
215
- def #{field_type}_field(method, **options, &block)
216
- ShoelaceInputField.new(:#{field_class}, object_name, method, @template, options.with_defaults(object: @object, label: label_text(method))).render(&block)
217
- end
218
- RUBY
219
- end
220
-
221
- def color_field(method, **options)
222
- ShoelaceColorPicker.new(object_name, method, @template, options.with_defaults(object: @object, label: label_text(method))).render
223
- end
224
- alias color_picker color_field
225
-
226
- def range_field(method, **options)
227
- ShoelaceRange.new(object_name, method, @template, options.with_defaults(object: @object, label: label_text(method))).render
228
- end
229
- alias range range_field
230
-
231
- def switch_field(method, **options, &block)
232
- if block_given?
233
- ShoelaceSwitch.new(object_name, method, @template, options.with_defaults(object: @object)).render(&block)
234
- else
235
- ShoelaceSwitch.new(object_name, method, @template, options.with_defaults(object: @object, label: label_text(method))).render(&block)
236
- end
237
- end
238
- alias switch switch_field
239
-
240
- def text_area(method, **options, &block)
241
- ShoelaceTextArea.new(object_name, method, @template, options.with_defaults(object: @object, label: label_text(method), resize: 'auto')).render(&block)
242
- end
243
-
244
- def check_box(method, options = {}, checked_value = "1", unchecked_value = "0", &block)
245
- ShoelaceCheckBox.new(object_name, method, @template, checked_value, unchecked_value, options.with_defaults(label: label_text(method)).merge(object: @object)).render(&block)
246
- end
247
-
248
- def select(method, choices = nil, options = {}, html_options = {}, &block)
249
- ShoelaceSelect.new(object_name, method, @template, choices, options.with_defaults(object: @object), html_options.with_defaults(label: label_text(method)), &block).render
250
- end
251
-
252
- def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
253
- ShoelaceCollectionSelect.new(object_name, method, @template, collection, value_method, text_method, options.with_defaults(object: @object), html_options.with_defaults(label: label_text(method)), &block).render
254
- end
255
-
256
- def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
257
- ShoelaceGroupedCollectionSelect.new(object_name, method, @template, collection, group_method, group_label_method, option_key_method, option_value_method, options.with_defaults(object: @object), html_options.with_defaults(label: label_text(method))).render
258
- end
259
-
260
- def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
261
- ShoelaceCollectionRadioButtons.new(object_name, method, @template, collection, value_method, text_method, options.with_defaults(object: @object, label: label_text(method)), html_options).render(&block)
262
- end
263
-
264
- def submit(value = nil, options = {})
265
- value, options = nil, value if value.is_a?(Hash)
266
-
267
- @template.sl_submit_tag(value || submit_default_value, **options)
268
- end
269
-
270
- private
271
-
272
- def label_text(method, tag_value = nil)
273
- ::ActionView::Helpers::Tags::Label::LabelBuilder.new(@template, object_name.to_s, method.to_s, object, tag_value).translation
274
- end
275
- end
276
-
277
- DEFAULT_FORM_PARAMETERS = { builder: ShoelaceFormBuilder }
278
- DIVIDER_TAG = "<sl-divider></sl-divider>".html_safe
279
-
280
- private_constant :DEFAULT_FORM_PARAMETERS, :DIVIDER_TAG
281
-
282
- def sl_form_for(*args, **options, &block)
283
- form_for(*args, **DEFAULT_FORM_PARAMETERS, **options, &block)
284
- end
285
-
286
- def sl_form_with(**args, &block)
287
- form_with(**args, **DEFAULT_FORM_PARAMETERS, &block)
288
- end
289
-
290
- # Creates a submit button with the text value as the caption, with the +submit+ attribute.
291
- def sl_submit_tag(value = 'Save changes', **options)
292
- options = options.deep_stringify_keys
293
- tag_options = { "type" => "submit", "variant" => "primary" }.update(options)
294
- set_default_disable_with(value, tag_options)
295
-
296
- content_tag('sl-button', value, tag_options)
297
- end
298
-
299
- # Creates a shoelace text field; use these text fields to input smaller chunks of text like a username or a search
300
- # query.
301
- #
302
- # For the properties available on this tag, please refer to the official documentation:
303
- # https://shoelace.style/components/input?id=properties
304
- #
305
- def sl_text_field_tag(name, value = nil, **options, &block)
306
- content_tag('sl-input', '', { "type" => "text", "name" => name, "id" => sanitize_to_id(name), "value" => value }.update(options.stringify_keys), &block)
307
- end
308
-
309
- # Returns a string of +<sl-option>+ tags, like +options_for_select+, but prepends a +<small>+ tag to
310
- # each group.
311
- def grouped_sl_options_for_select(grouped_options, options)
312
- body = "".html_safe
313
-
314
- grouped_options.each_with_index do |container, index|
315
- label, values = container
316
-
317
- body.safe_concat(DIVIDER_TAG) if index > 0
318
- body.safe_concat(content_tag("small", label)) if label.present?
319
- body.safe_concat(sl_options_for_select(values, options))
320
- end
321
-
322
- body
323
- end
324
-
325
- # Accepts an enumerable (hash, array, enumerable, your type) and returns a string of +sl-option+ tags. Given
326
- # an enumerable where the elements respond to +first+ and +last+ (such as a two-element array), the “lasts” serve
327
- # as option values and the “firsts” as option text.
328
- def sl_options_for_select(enumerable, options = nil)
329
- return enumerable if String === enumerable
330
-
331
- selected, disabled = extract_selected_and_disabled(options).map { |r| Array(r).map(&:to_s) }
332
-
333
- enumerable.map do |element|
334
- html_attributes = option_html_attributes(element)
335
- text, value = option_text_and_value(element).map(&:to_s)
336
-
337
- html_attributes[:checked] ||= selected.include?(value)
338
- html_attributes[:disabled] ||= disabled.include?(value)
339
- html_attributes[:value] = value
340
-
341
- tag_builder.content_tag_string('sl-option', text, html_attributes)
342
- end.join("\n").html_safe
343
- end
344
-
345
- def sl_option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
346
- body = "".html_safe
347
-
348
- collection.each_with_index do |group, index|
349
- option_tags = sl_options_from_collection_for_select(value_for_collection(group, group_method), option_key_method, option_value_method, selected_key)
350
-
351
- body.safe_concat(DIVIDER_TAG) if index > 0
352
- body.safe_concat(content_tag("small", value_for_collection(group, group_label_method)))
353
- body.safe_concat(option_tags)
354
- end
355
-
356
- body
357
- end
358
-
359
- # Returns a string of +<sl-option>+ tags compiled by iterating over the collection and assigning the result of
360
- # a call to the +value_method+ as the option value and the +text_method+ as the option text.
361
- def sl_options_from_collection_for_select(collection, value_method, text_method, selected = nil)
362
- options = collection.map do |element|
363
- [value_for_collection(element, text_method), value_for_collection(element, value_method), option_html_attributes(element)]
364
- end
365
-
366
- selected, disabled = extract_selected_and_disabled(selected)
367
-
368
- select_deselect = {
369
- selected: extract_values_from_collection(collection, value_method, selected),
370
- disabled: extract_values_from_collection(collection, value_method, disabled)
371
- }
372
-
373
- sl_options_for_select(options, select_deselect)
374
- end
375
-
376
- # Returns a +<sl-radio>+ tag for accessing a specified attribute (identified by method) on an object assigned to
377
- # the template (identified by object). If the current value of method is +tag_value+ the radio button will be
378
- # checked.
379
- #
380
- # To force the radio button to be checked pass checked: true in the options hash. You may pass HTML options there
381
- # as well.
382
- def sl_radio_button(object_name, method, tag_value, options = {}, &block)
383
- ShoelaceRadioButton.new(object_name, method, self, tag_value, options).render(&block)
384
- end
385
-
386
- {
387
- email: :email,
388
- number: :number,
389
- password: :password,
390
- search: :search,
391
- telephone: :tel,
392
- phone: :tel,
393
- url: :url
394
- }.each do |field_type, field_class|
395
- # def sl_email_field_tag(method, **options, &block)
396
- # sl_text_field_tag(name, value, options.merge(type: :email))
397
- # end
398
- eval <<-RUBY, nil, __FILE__, __LINE__ + 1
399
- # Creates a text field of type “#{field_type}”.
400
- def sl_#{field_type}_field_tag(method, **options, &block)
401
- sl_text_field_tag(name, value, options.merge(type: :#{field_class}))
402
- end
403
- RUBY
404
- end
405
- end
406
-
407
- FormBuilder = FormHelper::ShoelaceFormBuilder
408
- end