better_ui 0.6.0 → 0.7.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.
- checksums.yaml +4 -4
- data/README.md +257 -212
- data/Rakefile +11 -2
- data/app/components/better_ui/action_messages_component/action_messages_component.html.erb +48 -0
- data/app/components/better_ui/action_messages_component.rb +544 -0
- data/app/components/better_ui/application_component.rb +66 -0
- data/app/components/better_ui/button_component/button_component.html.erb +31 -0
- data/app/components/better_ui/button_component.rb +307 -0
- data/app/components/better_ui/card_component/card_component.html.erb +17 -0
- data/app/components/better_ui/card_component.rb +460 -0
- data/app/components/better_ui/drawer/header_component/header_component.html.erb +24 -0
- data/app/components/better_ui/drawer/header_component.rb +238 -0
- data/app/components/better_ui/drawer/layout_component/layout_component.html.erb +44 -0
- data/app/components/better_ui/drawer/layout_component.rb +270 -0
- data/app/components/better_ui/drawer/nav_group_component/nav_group_component.html.erb +10 -0
- data/app/components/better_ui/drawer/nav_group_component.rb +155 -0
- data/app/components/better_ui/drawer/nav_item_component/nav_item_component.html.erb +13 -0
- data/app/components/better_ui/drawer/nav_item_component.rb +225 -0
- data/app/components/better_ui/drawer/sidebar_component/sidebar_component.html.erb +17 -0
- data/app/components/better_ui/drawer/sidebar_component.rb +263 -0
- data/app/components/better_ui/forms/base_component.rb +450 -0
- data/app/components/better_ui/forms/checkbox_component/checkbox_component.html.erb +28 -0
- data/app/components/better_ui/forms/checkbox_component.rb +419 -0
- data/app/components/better_ui/forms/checkbox_group_component/checkbox_group_component.html.erb +40 -0
- data/app/components/better_ui/forms/checkbox_group_component.rb +363 -0
- data/app/components/better_ui/forms/number_input_component/number_input_component.html.erb +40 -0
- data/app/components/better_ui/forms/number_input_component.rb +320 -0
- data/app/components/better_ui/forms/password_input_component/password_input_component.html.erb +71 -0
- data/app/components/better_ui/forms/password_input_component.rb +206 -0
- data/app/components/better_ui/forms/text_input_component/text_input_component.html.erb +40 -0
- data/app/components/better_ui/forms/text_input_component.rb +258 -0
- data/app/components/better_ui/forms/textarea_component/textarea_component.html.erb +40 -0
- data/app/components/better_ui/forms/textarea_component.rb +329 -0
- data/app/form_builders/better_ui/ui_form_builder.rb +467 -0
- data/app/helpers/better_ui/application_helper.rb +325 -58
- data/app/views/layouts/better_ui/application.html.erb +1 -1
- data/config/routes.rb +1 -0
- data/lib/better_ui/engine.rb +34 -5
- data/lib/better_ui/version.rb +1 -1
- data/lib/better_ui.rb +32 -5
- data/lib/generators/better_ui/install/USAGE +44 -0
- data/lib/generators/better_ui/install/install_generator.rb +87 -0
- data/lib/generators/better_ui/install/templates/better_ui_theme.css.tt +280 -0
- data/lib/tasks/better_ui_tasks.rake +39 -4
- metadata +52 -200
- data/app/components/better_ui/application/card/component.html.erb +0 -20
- data/app/components/better_ui/application/card/component.rb +0 -214
- data/app/components/better_ui/application/main/component.html.erb +0 -9
- data/app/components/better_ui/application/main/component.rb +0 -123
- data/app/components/better_ui/application/navbar/component.html.erb +0 -92
- data/app/components/better_ui/application/navbar/component.rb +0 -136
- data/app/components/better_ui/application/sidebar/component.html.erb +0 -249
- data/app/components/better_ui/application/sidebar/component.rb +0 -187
- data/app/components/better_ui/general/accordion/component.html.erb +0 -5
- data/app/components/better_ui/general/accordion/component.rb +0 -92
- data/app/components/better_ui/general/accordion/item_component.html.erb +0 -12
- data/app/components/better_ui/general/accordion/item_component.rb +0 -176
- data/app/components/better_ui/general/alert/component.html.erb +0 -32
- data/app/components/better_ui/general/alert/component.rb +0 -242
- data/app/components/better_ui/general/avatar/component.html.erb +0 -20
- data/app/components/better_ui/general/avatar/component.rb +0 -301
- data/app/components/better_ui/general/badge/component.html.erb +0 -23
- data/app/components/better_ui/general/badge/component.rb +0 -248
- data/app/components/better_ui/general/breadcrumb/component.html.erb +0 -15
- data/app/components/better_ui/general/breadcrumb/component.rb +0 -187
- data/app/components/better_ui/general/button/component.html.erb +0 -34
- data/app/components/better_ui/general/button/component.rb +0 -214
- data/app/components/better_ui/general/divider/component.html.erb +0 -10
- data/app/components/better_ui/general/divider/component.rb +0 -226
- data/app/components/better_ui/general/dropdown/component.html.erb +0 -28
- data/app/components/better_ui/general/dropdown/component.rb +0 -192
- data/app/components/better_ui/general/dropdown/divider_component.html.erb +0 -1
- data/app/components/better_ui/general/dropdown/divider_component.rb +0 -41
- data/app/components/better_ui/general/dropdown/item_component.html.erb +0 -6
- data/app/components/better_ui/general/dropdown/item_component.rb +0 -119
- data/app/components/better_ui/general/field/component.html.erb +0 -27
- data/app/components/better_ui/general/field/component.rb +0 -37
- data/app/components/better_ui/general/grid/cell_component.html.erb +0 -3
- data/app/components/better_ui/general/grid/cell_component.rb +0 -390
- data/app/components/better_ui/general/grid/component.html.erb +0 -3
- data/app/components/better_ui/general/grid/component.rb +0 -301
- data/app/components/better_ui/general/heading/component.html.erb +0 -22
- data/app/components/better_ui/general/heading/component.rb +0 -257
- data/app/components/better_ui/general/icon/component.html.erb +0 -7
- data/app/components/better_ui/general/icon/component.rb +0 -240
- data/app/components/better_ui/general/input/checkbox/component.html.erb +0 -5
- data/app/components/better_ui/general/input/checkbox/component.rb +0 -238
- data/app/components/better_ui/general/input/datetime/component.html.erb +0 -5
- data/app/components/better_ui/general/input/datetime/component.rb +0 -223
- data/app/components/better_ui/general/input/pin/component.html.erb +0 -1
- data/app/components/better_ui/general/input/pin/component.rb +0 -201
- data/app/components/better_ui/general/input/radio/component.html.erb +0 -5
- data/app/components/better_ui/general/input/radio/component.rb +0 -230
- data/app/components/better_ui/general/input/rating/component.html.erb +0 -4
- data/app/components/better_ui/general/input/rating/component.rb +0 -272
- data/app/components/better_ui/general/input/select/component.html.erb +0 -78
- data/app/components/better_ui/general/input/select/component.rb +0 -249
- data/app/components/better_ui/general/input/select/select_component.html.erb +0 -5
- data/app/components/better_ui/general/input/select/select_component.rb +0 -37
- data/app/components/better_ui/general/input/text/component.html.erb +0 -5
- data/app/components/better_ui/general/input/text/component.rb +0 -171
- data/app/components/better_ui/general/input/textarea/component.html.erb +0 -5
- data/app/components/better_ui/general/input/textarea/component.rb +0 -166
- data/app/components/better_ui/general/input/toggle/component.html.erb +0 -5
- data/app/components/better_ui/general/input/toggle/component.rb +0 -242
- data/app/components/better_ui/general/link/component.html.erb +0 -18
- data/app/components/better_ui/general/link/component.rb +0 -258
- data/app/components/better_ui/general/modal/component.html.erb +0 -5
- data/app/components/better_ui/general/modal/component.rb +0 -47
- data/app/components/better_ui/general/modal/modal_component.html.erb +0 -52
- data/app/components/better_ui/general/modal/modal_component.rb +0 -160
- data/app/components/better_ui/general/pagination/component.html.erb +0 -85
- data/app/components/better_ui/general/pagination/component.rb +0 -216
- data/app/components/better_ui/general/panel/component.html.erb +0 -28
- data/app/components/better_ui/general/panel/component.rb +0 -249
- data/app/components/better_ui/general/progress/component.html.erb +0 -11
- data/app/components/better_ui/general/progress/component.rb +0 -160
- data/app/components/better_ui/general/spinner/component.html.erb +0 -35
- data/app/components/better_ui/general/spinner/component.rb +0 -93
- data/app/components/better_ui/general/table/component.html.erb +0 -5
- data/app/components/better_ui/general/table/component.rb +0 -217
- data/app/components/better_ui/general/table/tbody_component.html.erb +0 -3
- data/app/components/better_ui/general/table/tbody_component.rb +0 -30
- data/app/components/better_ui/general/table/td_component.html.erb +0 -3
- data/app/components/better_ui/general/table/td_component.rb +0 -44
- data/app/components/better_ui/general/table/tfoot_component.html.erb +0 -3
- data/app/components/better_ui/general/table/tfoot_component.rb +0 -28
- data/app/components/better_ui/general/table/th_component.html.erb +0 -6
- data/app/components/better_ui/general/table/th_component.rb +0 -51
- data/app/components/better_ui/general/table/thead_component.html.erb +0 -3
- data/app/components/better_ui/general/table/thead_component.rb +0 -28
- data/app/components/better_ui/general/table/tr_component.html.erb +0 -3
- data/app/components/better_ui/general/table/tr_component.rb +0 -30
- data/app/components/better_ui/general/tabs/component.html.erb +0 -11
- data/app/components/better_ui/general/tabs/component.rb +0 -120
- data/app/components/better_ui/general/tabs/panel_component.html.erb +0 -3
- data/app/components/better_ui/general/tabs/panel_component.rb +0 -37
- data/app/components/better_ui/general/tabs/tab_component.html.erb +0 -13
- data/app/components/better_ui/general/tabs/tab_component.rb +0 -111
- data/app/components/better_ui/general/tag/component.html.erb +0 -3
- data/app/components/better_ui/general/tag/component.rb +0 -104
- data/app/components/better_ui/general/text/component.html.erb +0 -1
- data/app/components/better_ui/general/text/component.rb +0 -194
- data/app/components/better_ui/general/tooltip/component.html.erb +0 -7
- data/app/components/better_ui/general/tooltip/component.rb +0 -239
- data/app/helpers/better_ui/application/components/card/card_helper.rb +0 -96
- data/app/helpers/better_ui/application/components/card.rb +0 -11
- data/app/helpers/better_ui/application/components/main/main_helper.rb +0 -64
- data/app/helpers/better_ui/application/components/navbar/navbar_helper.rb +0 -77
- data/app/helpers/better_ui/application/components/sidebar/sidebar_helper.rb +0 -51
- data/app/helpers/better_ui/general/components/accordion/accordion_helper.rb +0 -73
- data/app/helpers/better_ui/general/components/alert/alert_helper.rb +0 -57
- data/app/helpers/better_ui/general/components/avatar/avatar_helper.rb +0 -29
- data/app/helpers/better_ui/general/components/badge/badge_helper.rb +0 -53
- data/app/helpers/better_ui/general/components/breadcrumb/breadcrumb_helper.rb +0 -37
- data/app/helpers/better_ui/general/components/button/button_helper.rb +0 -65
- data/app/helpers/better_ui/general/components/container/container_helper.rb +0 -60
- data/app/helpers/better_ui/general/components/divider/divider_helper.rb +0 -63
- data/app/helpers/better_ui/general/components/dropdown/divider_helper.rb +0 -32
- data/app/helpers/better_ui/general/components/dropdown/dropdown_helper.rb +0 -88
- data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +0 -68
- data/app/helpers/better_ui/general/components/field/field_helper.rb +0 -26
- data/app/helpers/better_ui/general/components/grid/grid_helper.rb +0 -145
- data/app/helpers/better_ui/general/components/heading/heading_helper.rb +0 -72
- data/app/helpers/better_ui/general/components/icon/icon_helper.rb +0 -16
- data/app/helpers/better_ui/general/components/input/checkbox/checkbox_helper.rb +0 -81
- data/app/helpers/better_ui/general/components/input/datetime/datetime_helper.rb +0 -91
- data/app/helpers/better_ui/general/components/input/pin/pin_helper.rb +0 -76
- data/app/helpers/better_ui/general/components/input/radio/radio_helper.rb +0 -79
- data/app/helpers/better_ui/general/components/input/radio_group/radio_group_helper.rb +0 -124
- data/app/helpers/better_ui/general/components/input/rating/rating_helper.rb +0 -70
- data/app/helpers/better_ui/general/components/input/select/select_helper.rb +0 -86
- data/app/helpers/better_ui/general/components/input/text/text_helper.rb +0 -138
- data/app/helpers/better_ui/general/components/input/textarea/textarea_helper.rb +0 -73
- data/app/helpers/better_ui/general/components/input/toggle/toggle_helper.rb +0 -77
- data/app/helpers/better_ui/general/components/link/link_helper.rb +0 -89
- data/app/helpers/better_ui/general/components/modal/modal_helper.rb +0 -85
- data/app/helpers/better_ui/general/components/pagination/pagination_helper.rb +0 -82
- data/app/helpers/better_ui/general/components/panel/panel_helper.rb +0 -83
- data/app/helpers/better_ui/general/components/progress/progress_helper.rb +0 -53
- data/app/helpers/better_ui/general/components/spinner/spinner_helper.rb +0 -19
- data/app/helpers/better_ui/general/components/table/table_helper.rb +0 -53
- data/app/helpers/better_ui/general/components/table/tbody_helper.rb +0 -13
- data/app/helpers/better_ui/general/components/table/td_helper.rb +0 -19
- data/app/helpers/better_ui/general/components/table/tfoot_helper.rb +0 -13
- data/app/helpers/better_ui/general/components/table/th_helper.rb +0 -19
- data/app/helpers/better_ui/general/components/table/thead_helper.rb +0 -13
- data/app/helpers/better_ui/general/components/table/tr_helper.rb +0 -13
- data/app/helpers/better_ui/general/components/tabs/panel_helper.rb +0 -62
- data/app/helpers/better_ui/general/components/tabs/tab_helper.rb +0 -55
- data/app/helpers/better_ui/general/components/tabs/tabs_helper.rb +0 -95
- data/app/helpers/better_ui/general/components/tag/tag_helper.rb +0 -26
- data/app/helpers/better_ui/general/components/text/text_helper.rb +0 -83
- data/app/helpers/better_ui/general/components/tooltip/tooltip_helper.rb +0 -60
- data/app/jobs/better_ui/application_job.rb +0 -4
- data/app/mailers/better_ui/application_mailer.rb +0 -6
- data/config/initializers/lookbook.rb +0 -23
- data/lib/better_ui/railtie.rb +0 -20
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
# Custom Rails form builder for rendering form inputs using BetterUi::Forms components.
|
|
5
|
+
#
|
|
6
|
+
# This form builder integrates seamlessly with ActiveModel objects to automatically
|
|
7
|
+
# populate field values, validation errors, and required status from the model.
|
|
8
|
+
# All form inputs are rendered using ViewComponents from the BetterUi::Forms namespace,
|
|
9
|
+
# ensuring consistent styling and behavior across the application.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage with form_with
|
|
12
|
+
# <%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
|
|
13
|
+
# <%= f.bui_text_input :name %>
|
|
14
|
+
# <%= f.bui_text_input :email, hint: "We'll never share your email" %>
|
|
15
|
+
# <%= f.bui_number_input :age, min: 0, max: 120 %>
|
|
16
|
+
# <% end %>
|
|
17
|
+
#
|
|
18
|
+
# @example With custom labels and sizes
|
|
19
|
+
# <%= form_with model: @product, builder: BetterUi::UiFormBuilder do |f| %>
|
|
20
|
+
# <%= f.bui_text_input :title, label: "Product Name", size: :lg %>
|
|
21
|
+
# <%= f.bui_number_input :price, label: "Price ($)", min: 0, step: 0.01 %>
|
|
22
|
+
# <% end %>
|
|
23
|
+
#
|
|
24
|
+
# @example With icon slots
|
|
25
|
+
# <%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
|
|
26
|
+
# <%= f.bui_text_input :email do |component| %>
|
|
27
|
+
# <% component.with_prefix_icon do %>
|
|
28
|
+
# <svg class="h-5 w-5 text-gray-400">...</svg>
|
|
29
|
+
# <% end %>
|
|
30
|
+
# <% end %>
|
|
31
|
+
#
|
|
32
|
+
# <%= f.bui_number_input :budget do |component| %>
|
|
33
|
+
# <% component.with_prefix_icon { "$" } %>
|
|
34
|
+
# <% end %>
|
|
35
|
+
# <% end %>
|
|
36
|
+
#
|
|
37
|
+
# @example Automatic error handling
|
|
38
|
+
# # When @user has validation errors:
|
|
39
|
+
# <%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
|
|
40
|
+
# <%= f.bui_text_input :email %>
|
|
41
|
+
# # Automatically displays error messages and applies error styling
|
|
42
|
+
# <% end %>
|
|
43
|
+
#
|
|
44
|
+
# @see BetterUi::Forms::TextInputComponent
|
|
45
|
+
# @see BetterUi::Forms::NumberInputComponent
|
|
46
|
+
# @see BetterUi::Forms::PasswordInputComponent
|
|
47
|
+
# @see BetterUi::Forms::TextareaComponent
|
|
48
|
+
# @see BetterUi::Forms::BaseComponent
|
|
49
|
+
class UiFormBuilder < ActionView::Helpers::FormBuilder
|
|
50
|
+
# Renders a text input field using BetterUi::Forms::TextInputComponent
|
|
51
|
+
#
|
|
52
|
+
# @param attribute [Symbol] The attribute name
|
|
53
|
+
# @param options [Hash] Additional options to pass to the component
|
|
54
|
+
# @option options [String] :label Custom label text (defaults to humanized attribute name)
|
|
55
|
+
# @option options [String] :hint Hint text to display below the input
|
|
56
|
+
# @option options [String] :placeholder Placeholder text
|
|
57
|
+
# @option options [Symbol] :size Size variant (:xs, :sm, :md, :lg, :xl)
|
|
58
|
+
# @option options [Boolean] :disabled Whether the input is disabled
|
|
59
|
+
# @option options [Boolean] :readonly Whether the input is readonly
|
|
60
|
+
# @option options [Boolean] :required Whether the input is required
|
|
61
|
+
# @return [String] Rendered component HTML
|
|
62
|
+
#
|
|
63
|
+
# @example Basic usage
|
|
64
|
+
# <%= f.bui_text_input :email %>
|
|
65
|
+
#
|
|
66
|
+
# @example With options
|
|
67
|
+
# <%= f.bui_text_input :email, size: :lg, hint: "We'll never share your email" %>
|
|
68
|
+
#
|
|
69
|
+
# @example With icon
|
|
70
|
+
# <%= f.bui_text_input :email do |c| %>
|
|
71
|
+
# <% c.with_prefix_icon { "📧" } %>
|
|
72
|
+
# <% end %>
|
|
73
|
+
def bui_text_input(attribute, options = {}, &block)
|
|
74
|
+
component_options = build_input_options(attribute, options)
|
|
75
|
+
|
|
76
|
+
if block_given?
|
|
77
|
+
@template.render(BetterUi::Forms::TextInputComponent.new(**component_options), &block)
|
|
78
|
+
else
|
|
79
|
+
@template.render(BetterUi::Forms::TextInputComponent.new(**component_options))
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Renders a number input field using BetterUi::Forms::NumberInputComponent
|
|
84
|
+
#
|
|
85
|
+
# @param attribute [Symbol] The attribute name
|
|
86
|
+
# @param options [Hash] Additional options to pass to the component
|
|
87
|
+
# @option options [String] :label Custom label text (defaults to humanized attribute name)
|
|
88
|
+
# @option options [String] :hint Hint text to display below the input
|
|
89
|
+
# @option options [String] :placeholder Placeholder text
|
|
90
|
+
# @option options [Symbol] :size Size variant (:xs, :sm, :md, :lg, :xl)
|
|
91
|
+
# @option options [Boolean] :disabled Whether the input is disabled
|
|
92
|
+
# @option options [Boolean] :readonly Whether the input is readonly
|
|
93
|
+
# @option options [Boolean] :required Whether the input is required
|
|
94
|
+
# @option options [Numeric] :min Minimum value
|
|
95
|
+
# @option options [Numeric] :max Maximum value
|
|
96
|
+
# @option options [Numeric] :step Step value
|
|
97
|
+
# @option options [Boolean] :show_spinner Whether to show up/down spinner arrows (default: true)
|
|
98
|
+
# @return [String] Rendered component HTML
|
|
99
|
+
#
|
|
100
|
+
# @example Basic usage
|
|
101
|
+
# <%= f.bui_number_input :age %>
|
|
102
|
+
#
|
|
103
|
+
# @example With range and step
|
|
104
|
+
# <%= f.bui_number_input :price, min: 0, max: 10000, step: 0.01 %>
|
|
105
|
+
#
|
|
106
|
+
# @example Without spinners
|
|
107
|
+
# <%= f.bui_number_input :price, show_spinner: false %>
|
|
108
|
+
#
|
|
109
|
+
# @example With icon
|
|
110
|
+
# <%= f.bui_number_input :price do |c| %>
|
|
111
|
+
# <% c.with_prefix_icon { "$" } %>
|
|
112
|
+
# <% end %>
|
|
113
|
+
def bui_number_input(attribute, options = {}, &block)
|
|
114
|
+
component_options = build_input_options(attribute, options)
|
|
115
|
+
|
|
116
|
+
# Add number-specific options
|
|
117
|
+
component_options[:min] = options[:min] if options.key?(:min)
|
|
118
|
+
component_options[:max] = options[:max] if options.key?(:max)
|
|
119
|
+
component_options[:step] = options[:step] if options.key?(:step)
|
|
120
|
+
component_options[:show_spinner] = options.fetch(:show_spinner, true)
|
|
121
|
+
|
|
122
|
+
if block_given?
|
|
123
|
+
@template.render(BetterUi::Forms::NumberInputComponent.new(**component_options), &block)
|
|
124
|
+
else
|
|
125
|
+
@template.render(BetterUi::Forms::NumberInputComponent.new(**component_options))
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Renders a password input field using BetterUi::Forms::PasswordInputComponent
|
|
130
|
+
#
|
|
131
|
+
# @param attribute [Symbol] The attribute name
|
|
132
|
+
# @param options [Hash] Additional options to pass to the component
|
|
133
|
+
# @option options [String] :label Custom label text (defaults to humanized attribute name)
|
|
134
|
+
# @option options [String] :hint Hint text to display below the input
|
|
135
|
+
# @option options [String] :placeholder Placeholder text
|
|
136
|
+
# @option options [Symbol] :size Size variant (:xs, :sm, :md, :lg, :xl)
|
|
137
|
+
# @option options [Boolean] :disabled Whether the input is disabled
|
|
138
|
+
# @option options [Boolean] :readonly Whether the input is readonly
|
|
139
|
+
# @option options [Boolean] :required Whether the input is required
|
|
140
|
+
# @return [String] Rendered component HTML
|
|
141
|
+
#
|
|
142
|
+
# @example Basic usage
|
|
143
|
+
# <%= f.bui_password_input :password %>
|
|
144
|
+
#
|
|
145
|
+
# @example With confirmation field
|
|
146
|
+
# <%= f.bui_password_input :password, hint: "Must be at least 8 characters" %>
|
|
147
|
+
# <%= f.bui_password_input :password_confirmation %>
|
|
148
|
+
#
|
|
149
|
+
# @example With icon
|
|
150
|
+
# <%= f.bui_password_input :password do |c| %>
|
|
151
|
+
# <% c.with_prefix_icon do %>
|
|
152
|
+
# <svg class="h-5 w-5 text-gray-400">...</svg>
|
|
153
|
+
# <% end %>
|
|
154
|
+
# <% end %>
|
|
155
|
+
def bui_password_input(attribute, options = {}, &block)
|
|
156
|
+
component_options = build_input_options(attribute, options)
|
|
157
|
+
|
|
158
|
+
if block_given?
|
|
159
|
+
@template.render(BetterUi::Forms::PasswordInputComponent.new(**component_options), &block)
|
|
160
|
+
else
|
|
161
|
+
@template.render(BetterUi::Forms::PasswordInputComponent.new(**component_options))
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Renders a textarea field using BetterUi::Forms::TextareaComponent
|
|
166
|
+
#
|
|
167
|
+
# @param attribute [Symbol] The attribute name
|
|
168
|
+
# @param options [Hash] Additional options to pass to the component
|
|
169
|
+
# @option options [String] :label Custom label text (defaults to humanized attribute name)
|
|
170
|
+
# @option options [String] :hint Hint text to display below the textarea
|
|
171
|
+
# @option options [String] :placeholder Placeholder text
|
|
172
|
+
# @option options [Symbol] :size Size variant (:xs, :sm, :md, :lg, :xl)
|
|
173
|
+
# @option options [Boolean] :disabled Whether the textarea is disabled
|
|
174
|
+
# @option options [Boolean] :readonly Whether the textarea is readonly
|
|
175
|
+
# @option options [Boolean] :required Whether the textarea is required
|
|
176
|
+
# @option options [Integer] :rows Number of visible text lines (default: 4)
|
|
177
|
+
# @option options [Integer] :cols Width in characters (optional)
|
|
178
|
+
# @option options [Integer] :maxlength Maximum number of characters allowed
|
|
179
|
+
# @option options [Symbol] :resize CSS resize behavior (:none, :vertical, :horizontal, :both)
|
|
180
|
+
# @return [String] Rendered component HTML
|
|
181
|
+
#
|
|
182
|
+
# @example Basic usage
|
|
183
|
+
# <%= f.bui_textarea :description %>
|
|
184
|
+
#
|
|
185
|
+
# @example With custom rows and maxlength
|
|
186
|
+
# <%= f.bui_textarea :bio, rows: 6, maxlength: 500, hint: "Maximum 500 characters" %>
|
|
187
|
+
#
|
|
188
|
+
# @example With resize disabled
|
|
189
|
+
# <%= f.bui_textarea :notes, resize: :none %>
|
|
190
|
+
#
|
|
191
|
+
# @example With icon
|
|
192
|
+
# <%= f.bui_textarea :comment do |c| %>
|
|
193
|
+
# <% c.with_prefix_icon do %>
|
|
194
|
+
# <svg class="h-5 w-5 text-gray-400">...</svg>
|
|
195
|
+
# <% end %>
|
|
196
|
+
# <% end %>
|
|
197
|
+
def bui_textarea(attribute, options = {}, &block)
|
|
198
|
+
component_options = build_input_options(attribute, options)
|
|
199
|
+
|
|
200
|
+
# Add textarea-specific options
|
|
201
|
+
component_options[:rows] = options.fetch(:rows, 4)
|
|
202
|
+
component_options[:cols] = options[:cols] if options.key?(:cols)
|
|
203
|
+
component_options[:maxlength] = options[:maxlength] if options.key?(:maxlength)
|
|
204
|
+
component_options[:resize] = options.fetch(:resize, :vertical)
|
|
205
|
+
|
|
206
|
+
if block_given?
|
|
207
|
+
@template.render(BetterUi::Forms::TextareaComponent.new(**component_options), &block)
|
|
208
|
+
else
|
|
209
|
+
@template.render(BetterUi::Forms::TextareaComponent.new(**component_options))
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Renders a checkbox input field using BetterUi::Forms::CheckboxComponent
|
|
214
|
+
#
|
|
215
|
+
# @param attribute [Symbol] The attribute name
|
|
216
|
+
# @param options [Hash] Additional options to pass to the component
|
|
217
|
+
# @option options [String] :label Custom label text (defaults to humanized attribute name)
|
|
218
|
+
# @option options [String] :value The value submitted when checkbox is checked (default: "1")
|
|
219
|
+
# @option options [String] :hint Hint text to display below the checkbox
|
|
220
|
+
# @option options [Symbol] :variant Color variant (:primary, :secondary, :accent, :success, :danger, :warning, :info, :light, :dark)
|
|
221
|
+
# @option options [Symbol] :size Size variant (:xs, :sm, :md, :lg, :xl)
|
|
222
|
+
# @option options [Symbol] :label_position Position of label relative to checkbox (:left, :right)
|
|
223
|
+
# @option options [Boolean] :disabled Whether the checkbox is disabled
|
|
224
|
+
# @option options [Boolean] :readonly Whether the checkbox is readonly
|
|
225
|
+
# @option options [Boolean] :required Whether the checkbox is required
|
|
226
|
+
# @return [String] Rendered component HTML
|
|
227
|
+
#
|
|
228
|
+
# @example Basic usage
|
|
229
|
+
# <%= f.bui_checkbox :newsletter %>
|
|
230
|
+
#
|
|
231
|
+
# @example With custom label
|
|
232
|
+
# <%= f.bui_checkbox :terms, label: "I agree to the terms and conditions" %>
|
|
233
|
+
#
|
|
234
|
+
# @example With variant
|
|
235
|
+
# <%= f.bui_checkbox :active, variant: :success, label: "Active" %>
|
|
236
|
+
def bui_checkbox(attribute, options = {})
|
|
237
|
+
component_options = build_checkbox_options(attribute, options)
|
|
238
|
+
|
|
239
|
+
@template.render(BetterUi::Forms::CheckboxComponent.new(**component_options))
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Renders a checkbox group using BetterUi::Forms::CheckboxGroupComponent
|
|
243
|
+
#
|
|
244
|
+
# @param attribute [Symbol] The attribute name
|
|
245
|
+
# @param collection [Array] The collection of options, can be:
|
|
246
|
+
# - Array of values (e.g., ["Admin", "Editor"])
|
|
247
|
+
# - Array of [label, value] pairs (e.g., [["Admin", "admin"], ["Editor", "editor"]])
|
|
248
|
+
# @param options [Hash] Additional options to pass to the component
|
|
249
|
+
# @option options [String] :legend Legend text for the fieldset (defaults to humanized attribute name)
|
|
250
|
+
# @option options [String] :hint Hint text to display below the checkboxes
|
|
251
|
+
# @option options [Symbol] :variant Color variant for all checkboxes (:primary, :secondary, etc.)
|
|
252
|
+
# @option options [Symbol] :size Size variant (:xs, :sm, :md, :lg, :xl)
|
|
253
|
+
# @option options [Symbol] :orientation Layout orientation (:vertical, :horizontal)
|
|
254
|
+
# @option options [Boolean] :disabled Whether all checkboxes are disabled
|
|
255
|
+
# @option options [Boolean] :required Whether the field is required
|
|
256
|
+
# @return [String] Rendered component HTML
|
|
257
|
+
#
|
|
258
|
+
# @example Basic usage
|
|
259
|
+
# <%= f.bui_checkbox_group :roles, ["Admin", "Editor", "Viewer"] %>
|
|
260
|
+
#
|
|
261
|
+
# @example With label/value pairs
|
|
262
|
+
# <%= f.bui_checkbox_group :permissions, [["Read", "read"], ["Write", "write"]] %>
|
|
263
|
+
#
|
|
264
|
+
# @example Horizontal layout
|
|
265
|
+
# <%= f.bui_checkbox_group :interests, ["Sports", "Music", "Art"], orientation: :horizontal %>
|
|
266
|
+
def bui_checkbox_group(attribute, collection, options = {})
|
|
267
|
+
component_options = build_checkbox_group_options(attribute, collection, options)
|
|
268
|
+
|
|
269
|
+
@template.render(BetterUi::Forms::CheckboxGroupComponent.new(**component_options))
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
private
|
|
273
|
+
|
|
274
|
+
# Builds common options for input components.
|
|
275
|
+
#
|
|
276
|
+
# Extracts and merges options from the form model, user-provided options,
|
|
277
|
+
# and default values. Automatically populates field name, value, errors,
|
|
278
|
+
# and required status from the ActiveModel object.
|
|
279
|
+
#
|
|
280
|
+
# @param attribute [Symbol] the attribute name
|
|
281
|
+
# @param options [Hash] user-provided options to override defaults
|
|
282
|
+
# @return [Hash] complete hash of component options ready for rendering
|
|
283
|
+
# @api private
|
|
284
|
+
def build_input_options(attribute, options)
|
|
285
|
+
{
|
|
286
|
+
name: field_name(attribute),
|
|
287
|
+
value: object_value(attribute),
|
|
288
|
+
label: options.fetch(:label, attribute.to_s.humanize),
|
|
289
|
+
hint: options[:hint],
|
|
290
|
+
placeholder: options[:placeholder],
|
|
291
|
+
size: options.fetch(:size, :md),
|
|
292
|
+
disabled: options.fetch(:disabled, false),
|
|
293
|
+
readonly: options.fetch(:readonly, false),
|
|
294
|
+
required: options.fetch(:required, field_required?(attribute)),
|
|
295
|
+
errors: object_errors(attribute),
|
|
296
|
+
container_classes: options[:container_classes],
|
|
297
|
+
label_classes: options[:label_classes],
|
|
298
|
+
input_classes: options[:input_classes],
|
|
299
|
+
hint_classes: options[:hint_classes],
|
|
300
|
+
error_classes: options[:error_classes]
|
|
301
|
+
}.merge(extract_html_attributes(options))
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Gets the value of an attribute from the object.
|
|
305
|
+
#
|
|
306
|
+
# Safely retrieves the attribute value from the form's model object,
|
|
307
|
+
# returning nil if the object doesn't respond to the attribute method.
|
|
308
|
+
#
|
|
309
|
+
# @param attribute [Symbol] the attribute name
|
|
310
|
+
# @return [Object, nil] the attribute value, or nil if not accessible
|
|
311
|
+
# @api private
|
|
312
|
+
def object_value(attribute)
|
|
313
|
+
@object&.public_send(attribute)
|
|
314
|
+
rescue NoMethodError
|
|
315
|
+
nil
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Gets validation errors for an attribute.
|
|
319
|
+
#
|
|
320
|
+
# Retrieves full error messages for the specified attribute from the
|
|
321
|
+
# model's errors object. Returns empty array if model doesn't support errors.
|
|
322
|
+
#
|
|
323
|
+
# @param attribute [Symbol] the attribute name
|
|
324
|
+
# @return [Array<String>] array of full error messages for the attribute
|
|
325
|
+
# @api private
|
|
326
|
+
def object_errors(attribute)
|
|
327
|
+
return [] unless @object&.respond_to?(:errors)
|
|
328
|
+
|
|
329
|
+
@object.errors.full_messages_for(attribute)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Determines if a field is required based on model validations.
|
|
333
|
+
#
|
|
334
|
+
# Inspects the model's validators to detect presence validators on the
|
|
335
|
+
# specified attribute. Returns true if a PresenceValidator is found.
|
|
336
|
+
#
|
|
337
|
+
# @param attribute [Symbol] the attribute name
|
|
338
|
+
# @return [Boolean] true if the field has a presence validator, false otherwise
|
|
339
|
+
# @api private
|
|
340
|
+
def field_required?(attribute)
|
|
341
|
+
return false unless @object.class.respond_to?(:validators_on)
|
|
342
|
+
|
|
343
|
+
validators = @object.class.validators_on(attribute)
|
|
344
|
+
validators.any? { |v| v.is_a?(ActiveModel::Validations::PresenceValidator) }
|
|
345
|
+
rescue NoMethodError
|
|
346
|
+
false
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Extracts HTML attributes from options.
|
|
350
|
+
#
|
|
351
|
+
# Filters user-provided options to extract standard HTML attributes
|
|
352
|
+
# (id, class, data, aria, etc.) that should be passed through to the
|
|
353
|
+
# input element.
|
|
354
|
+
#
|
|
355
|
+
# @param options [Hash] user-provided options
|
|
356
|
+
# @return [Hash] hash of HTML attributes to pass through
|
|
357
|
+
# @api private
|
|
358
|
+
def extract_html_attributes(options)
|
|
359
|
+
html_attrs = {}
|
|
360
|
+
|
|
361
|
+
# Pass through common HTML attributes
|
|
362
|
+
html_attrs[:id] = options[:id] if options.key?(:id)
|
|
363
|
+
html_attrs[:class] = options[:class] if options.key?(:class)
|
|
364
|
+
html_attrs[:data] = options[:data] if options.key?(:data)
|
|
365
|
+
html_attrs[:aria] = options[:aria] if options.key?(:aria)
|
|
366
|
+
html_attrs[:autocomplete] = options[:autocomplete] if options.key?(:autocomplete)
|
|
367
|
+
html_attrs[:maxlength] = options[:maxlength] if options.key?(:maxlength)
|
|
368
|
+
html_attrs[:pattern] = options[:pattern] if options.key?(:pattern)
|
|
369
|
+
|
|
370
|
+
html_attrs
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Generates the field name for form submission.
|
|
374
|
+
#
|
|
375
|
+
# Creates the proper HTML name attribute for the input field, following
|
|
376
|
+
# Rails conventions for nested attributes (e.g., "user[email]").
|
|
377
|
+
# If no object_name is present, returns the attribute as a string.
|
|
378
|
+
#
|
|
379
|
+
# @param attribute [Symbol] the attribute name
|
|
380
|
+
# @return [String] the properly formatted field name for form submission
|
|
381
|
+
# @api private
|
|
382
|
+
# @example With object name
|
|
383
|
+
# field_name(:email) # => "user[email]" (when object_name is "user")
|
|
384
|
+
# @example Without object name
|
|
385
|
+
# field_name(:email) # => "email"
|
|
386
|
+
def field_name(attribute)
|
|
387
|
+
@object_name ? "#{@object_name}[#{attribute}]" : attribute.to_s
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Builds options for checkbox components.
|
|
391
|
+
#
|
|
392
|
+
# Creates the complete options hash for CheckboxComponent including
|
|
393
|
+
# auto-populated values from the model for checked state and errors.
|
|
394
|
+
#
|
|
395
|
+
# @param attribute [Symbol] the attribute name
|
|
396
|
+
# @param options [Hash] user-provided options to override defaults
|
|
397
|
+
# @return [Hash] complete hash of component options ready for rendering
|
|
398
|
+
# @api private
|
|
399
|
+
def build_checkbox_options(attribute, options)
|
|
400
|
+
{
|
|
401
|
+
name: field_name(attribute),
|
|
402
|
+
value: options.fetch(:value, "1"),
|
|
403
|
+
checked: checkbox_checked?(attribute),
|
|
404
|
+
label: options.fetch(:label, attribute.to_s.humanize),
|
|
405
|
+
hint: options[:hint],
|
|
406
|
+
variant: options.fetch(:variant, :primary),
|
|
407
|
+
size: options.fetch(:size, :md),
|
|
408
|
+
label_position: options.fetch(:label_position, :right),
|
|
409
|
+
disabled: options.fetch(:disabled, false),
|
|
410
|
+
readonly: options.fetch(:readonly, false),
|
|
411
|
+
required: options.fetch(:required, field_required?(attribute)),
|
|
412
|
+
errors: object_errors(attribute),
|
|
413
|
+
container_classes: options[:container_classes],
|
|
414
|
+
label_classes: options[:label_classes],
|
|
415
|
+
checkbox_classes: options[:checkbox_classes],
|
|
416
|
+
hint_classes: options[:hint_classes],
|
|
417
|
+
error_classes: options[:error_classes]
|
|
418
|
+
}.merge(extract_html_attributes(options))
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Builds options for checkbox group components.
|
|
422
|
+
#
|
|
423
|
+
# Creates the complete options hash for CheckboxGroupComponent including
|
|
424
|
+
# auto-populated values from the model for selected values and errors.
|
|
425
|
+
#
|
|
426
|
+
# @param attribute [Symbol] the attribute name
|
|
427
|
+
# @param collection [Array] the collection of options
|
|
428
|
+
# @param options [Hash] user-provided options to override defaults
|
|
429
|
+
# @return [Hash] complete hash of component options ready for rendering
|
|
430
|
+
# @api private
|
|
431
|
+
def build_checkbox_group_options(attribute, collection, options)
|
|
432
|
+
{
|
|
433
|
+
name: field_name(attribute),
|
|
434
|
+
collection: collection,
|
|
435
|
+
selected: Array(object_value(attribute)),
|
|
436
|
+
legend: options.fetch(:legend, attribute.to_s.humanize),
|
|
437
|
+
hint: options[:hint],
|
|
438
|
+
variant: options.fetch(:variant, :primary),
|
|
439
|
+
size: options.fetch(:size, :md),
|
|
440
|
+
orientation: options.fetch(:orientation, :vertical),
|
|
441
|
+
disabled: options.fetch(:disabled, false),
|
|
442
|
+
required: options.fetch(:required, field_required?(attribute)),
|
|
443
|
+
errors: object_errors(attribute),
|
|
444
|
+
container_classes: options[:container_classes],
|
|
445
|
+
legend_classes: options[:legend_classes],
|
|
446
|
+
items_classes: options[:items_classes],
|
|
447
|
+
hint_classes: options[:hint_classes],
|
|
448
|
+
error_classes: options[:error_classes]
|
|
449
|
+
}.merge(extract_html_attributes(options))
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Determines if a checkbox should be checked based on the model's attribute value.
|
|
453
|
+
#
|
|
454
|
+
# For boolean attributes, returns the boolean value.
|
|
455
|
+
# For other types, checks if the value is present/truthy.
|
|
456
|
+
#
|
|
457
|
+
# @param attribute [Symbol] the attribute name
|
|
458
|
+
# @return [Boolean] true if the checkbox should be checked, false otherwise
|
|
459
|
+
# @api private
|
|
460
|
+
def checkbox_checked?(attribute)
|
|
461
|
+
value = object_value(attribute)
|
|
462
|
+
return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
463
|
+
|
|
464
|
+
value.present?
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|