rails-active-ui 0.3.5 → 0.3.7
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 +148 -0
- data/app/helpers/ui/fomantic_form_builder.rb +487 -0
- data/app/lib/component.rb +4 -2
- data/lib/ui/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc0cc8da3df26f99e7a8cc99484217887250d5012cf7dabb3c226ce44a0c0dfa
|
|
4
|
+
data.tar.gz: fe3478bbc49e83fd764b9731d45938d3a211106c80c17efc93d7a084e5930db2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c6a8ca84a74bd5d08eb650e612187dc09d7e5b11b1559b219a883e6846860018ae8e0238f5a653be4cd4d686f8dc34200dce6258f0e90d9e98511f66773c4808
|
|
7
|
+
data.tar.gz: 03ead102486b56deee8c6bd01db61a09324d5bfd25f150b33aaf144ad3d0823897fd0186e8b1211d919861a303d3df50cafa0b1f3666966449772655f634da3d
|
data/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# rails-active-ui
|
|
2
|
+
|
|
3
|
+
A Fomantic-UI component system for Rails. Views use `.html.ruby` files with PascalCase component calls.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "rails-active-ui"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Engine initializers
|
|
14
|
+
|
|
15
|
+
The gem's engine (`Ui::Engine`) registers the following automatically:
|
|
16
|
+
|
|
17
|
+
**Autoload paths** -- `app/lib` and `app/blocks` are added to the autoload paths so `Component` and block classes are available everywhere.
|
|
18
|
+
|
|
19
|
+
**Asset paths** -- `formantic-ui/` (Fomantic-UI CSS/JS distribution) and `app/javascript/` (Stimulus controllers) are added to Propshaft's asset paths. Reference them in your layout:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# CSS
|
|
23
|
+
StylesheetLink("stylesheets.css") # Fomantic-UI stylesheet
|
|
24
|
+
|
|
25
|
+
# jQuery + Fomantic-UI component JS (must come before importmap)
|
|
26
|
+
text fui_javascript_tags
|
|
27
|
+
|
|
28
|
+
# Importmap (loads Stimulus controllers)
|
|
29
|
+
JavascriptImportmap()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Importmap** -- the gem's `config/importmap.rb` is prepended to the app's importmap. It pins:
|
|
33
|
+
- `ui` -- the main entry point (`ui/index.js`)
|
|
34
|
+
- `ui/controllers/*` -- all Fomantic-UI Stimulus bridge controllers
|
|
35
|
+
- `emoji-picker-element` -- emoji picker from CDN
|
|
36
|
+
|
|
37
|
+
**Helpers** -- `ComponentHelper` and `FuiHelper` are included into `ActionView::Base` automatically.
|
|
38
|
+
|
|
39
|
+
### Stimulus controllers
|
|
40
|
+
|
|
41
|
+
Register the Fomantic-UI Stimulus controllers in your app's `app/javascript/controllers/index.js`:
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
import { Application } from "@hotwired/stimulus"
|
|
45
|
+
import { registerFuiControllers } from "ui"
|
|
46
|
+
|
|
47
|
+
const application = Application.start()
|
|
48
|
+
registerFuiControllers(application)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
These are thin jQuery bridge controllers that initialize Fomantic-UI widgets in `connect()` and tear them down in `disconnect()`, making them Turbo-compatible.
|
|
52
|
+
|
|
53
|
+
### Rails engine usage
|
|
54
|
+
|
|
55
|
+
If you're using rails-active-ui inside a Rails engine, your engine needs to register the gem's assets manually since engines don't inherit the host app's asset paths:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# lib/my_engine/engine.rb
|
|
59
|
+
class Engine < ::Rails::Engine
|
|
60
|
+
initializer "my_engine.assets" do |app|
|
|
61
|
+
ui_gem = Gem::Specification.find_by_name("rails-active-ui")
|
|
62
|
+
app.config.assets.paths << File.join(ui_gem.gem_dir, "app/assets")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Form Builder
|
|
68
|
+
|
|
69
|
+
rails-active-ui ships with `FomanticFormBuilder`, a drop-in `ActionView::Helpers::FormBuilder` subclass that wraps every field helper in Fomantic-UI markup.
|
|
70
|
+
|
|
71
|
+
Set it as the default in your `ApplicationController`:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
class ApplicationController < ActionController::Base
|
|
75
|
+
ActionView::Base.default_form_builder = FomanticFormBuilder
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Inside a `Form()` block, method_missing delegates to the form builder. Standard Rails form helpers become PascalCase calls:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
Form(url: users_path, method: :post) {
|
|
83
|
+
TextField(:name, required: true)
|
|
84
|
+
EmailField(:email)
|
|
85
|
+
Select(:role, [["Admin", "admin"], ["User", "user"]], dropdown: true)
|
|
86
|
+
CheckBox(:terms, label: "I agree to the Terms")
|
|
87
|
+
HiddenField(:token)
|
|
88
|
+
Submit("Save", color: "green")
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Available form helpers
|
|
93
|
+
|
|
94
|
+
| `.html.ruby` call | Form builder method | Description |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `TextField(:name)` | `f.text_field :name` | Text input wrapped in `.field` |
|
|
97
|
+
| `EmailField(:email)` | `f.email_field :email` | Email input |
|
|
98
|
+
| `PasswordField(:password)` | `f.password_field :password` | Password input |
|
|
99
|
+
| `NumberField(:age)` | `f.number_field :age` | Number input |
|
|
100
|
+
| `TextArea(:bio)` | `f.text_area :bio` | Textarea |
|
|
101
|
+
| `Select(:role, choices)` | `f.select :role, choices` | Select dropdown |
|
|
102
|
+
| `CheckBox(:terms)` | `f.check_box :terms` | Checkbox with Fomantic styling |
|
|
103
|
+
| `RadioButton(:plan, "pro")` | `f.radio_button :plan, "pro"` | Radio button |
|
|
104
|
+
| `HiddenField(:token)` | `f.hidden_field :token` | Hidden input (no wrapper) |
|
|
105
|
+
| `FileField(:avatar)` | `f.file_field :avatar` | File upload |
|
|
106
|
+
| `Submit("Save")` | `f.submit "Save"` | Submit button with Fomantic styling |
|
|
107
|
+
|
|
108
|
+
### Field options
|
|
109
|
+
|
|
110
|
+
All field helpers accept these options:
|
|
111
|
+
|
|
112
|
+
- `label:` -- override label text (`nil` to suppress)
|
|
113
|
+
- `required:` -- adds "required" class and asterisk
|
|
114
|
+
- `disabled:` -- adds "disabled" class
|
|
115
|
+
- `inline:` -- label sits beside the input
|
|
116
|
+
- `width:` -- Fomantic grid column word (e.g. `"six"`, `"three"`)
|
|
117
|
+
- `error:` -- error message string
|
|
118
|
+
- `hint:` -- grey note beneath the input
|
|
119
|
+
- `field_class:` -- extra classes on the wrapping `.field` div
|
|
120
|
+
- `input_class:` -- extra classes on the input element
|
|
121
|
+
|
|
122
|
+
### Submit button options
|
|
123
|
+
|
|
124
|
+
- `color:` -- Fomantic color (e.g. `"green"`, `"red"`, `"blue"`)
|
|
125
|
+
- `size:` -- Fomantic size (e.g. `"tiny"`, `"large"`)
|
|
126
|
+
- `basic:` -- basic button style
|
|
127
|
+
- `icon:` -- icon name (e.g. `"checkmark"`)
|
|
128
|
+
|
|
129
|
+
### Field groups
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
Form(url: users_path) {
|
|
133
|
+
FieldsGroup(equal_width: true) {
|
|
134
|
+
TextField(:first_name)
|
|
135
|
+
TextField(:last_name)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Form-level messages
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
Form(url: users_path) {
|
|
144
|
+
ErrorMessage("Something went wrong", ["Email is taken"])
|
|
145
|
+
SuccessMessage("All done!", "Profile updated.")
|
|
146
|
+
WarningMessage("Heads up", ["Verify your email"])
|
|
147
|
+
}
|
|
148
|
+
```
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# FomanticFormBuilder
|
|
4
|
+
#
|
|
5
|
+
# A Rails FormBuilder wrapping every helper in Fomantic-UI markup.
|
|
6
|
+
#
|
|
7
|
+
# Usage in a view:
|
|
8
|
+
#
|
|
9
|
+
# <%= form_with model: @user, builder: FomanticFormBuilder do |f| %>
|
|
10
|
+
# <%= f.text_field :name %>
|
|
11
|
+
# <%= f.email_field :email, required: true %>
|
|
12
|
+
# <%= f.select :role, [['Admin', 'admin'], ['User', 'user']], dropdown: true %>
|
|
13
|
+
# <%= f.check_box :terms, label: 'I agree to the Terms and Conditions' %>
|
|
14
|
+
# <%= f.fields_group(equal_width: true) do %>
|
|
15
|
+
# <%= f.text_field :first_name %>
|
|
16
|
+
# <%= f.text_field :last_name %>
|
|
17
|
+
# <% end %>
|
|
18
|
+
# <%= f.submit 'Save', color: 'green' %>
|
|
19
|
+
# <% end %>
|
|
20
|
+
#
|
|
21
|
+
# Field options (shared across all helpers):
|
|
22
|
+
# label: String – override label text (nil to suppress label)
|
|
23
|
+
# required: Boolean – adds "required" class and asterisk
|
|
24
|
+
# disabled: Boolean – adds "disabled" class
|
|
25
|
+
# readonly: Boolean – adds "read-only" class
|
|
26
|
+
# inline: Boolean – label sits beside the input
|
|
27
|
+
# width: String – Fomantic grid column word, e.g. "six", "three"
|
|
28
|
+
# error: String – error message; adds "error" class + inline message
|
|
29
|
+
# warning: String – warning message; adds "warning" class + inline message
|
|
30
|
+
# hint: String – rendered as a small grey note beneath the input
|
|
31
|
+
# field_class: String – extra classes on the wrapping .field div
|
|
32
|
+
# input_class: String – extra classes on the input element itself
|
|
33
|
+
#
|
|
34
|
+
module Ui
|
|
35
|
+
class FomanticFormBuilder < ActionView::Helpers::FormBuilder
|
|
36
|
+
COLUMN_WORDS = %w[
|
|
37
|
+
one two three four five six seven eight nine ten
|
|
38
|
+
eleven twelve thirteen fourteen fifteen sixteen
|
|
39
|
+
].freeze
|
|
40
|
+
|
|
41
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
42
|
+
# Text-like inputs
|
|
43
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
%i[
|
|
46
|
+
text_field email_field password_field
|
|
47
|
+
number_field url_field telephone_field phone_field
|
|
48
|
+
search_field color_field date_field datetime_local_field
|
|
49
|
+
month_field week_field time_field range_field
|
|
50
|
+
].each do |method_name|
|
|
51
|
+
define_method(method_name) do |attribute, options = {}|
|
|
52
|
+
fomantic_field(attribute, options) do |attr, opts|
|
|
53
|
+
opts[:class] = class_names("", opts.delete(:input_class))
|
|
54
|
+
super(attr, opts)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
60
|
+
# Text area
|
|
61
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
def text_area(attribute, options = {})
|
|
64
|
+
transparent = options.delete(:transparent)
|
|
65
|
+
fomantic_field(attribute, options) do |attr, opts|
|
|
66
|
+
input_class = class_names(("transparent" if transparent), opts.delete(:input_class))
|
|
67
|
+
opts[:class] = input_class.presence
|
|
68
|
+
super(attr, opts)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
73
|
+
# Emoji picker
|
|
74
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
def emoji_field(attribute, options = {})
|
|
77
|
+
fomantic_field(attribute, options) do |attr, opts|
|
|
78
|
+
current = object&.public_send(attr) rescue nil
|
|
79
|
+
name = object_name ? "#{object_name}[#{attr}]" : attr.to_s
|
|
80
|
+
|
|
81
|
+
@template.tag.div(data: { controller: "fui-emoji-picker" }) {
|
|
82
|
+
@template.safe_join([
|
|
83
|
+
@template.hidden_field_tag(name, current, data: { fui_emoji_picker_target: "input" }),
|
|
84
|
+
@template.tag.button(
|
|
85
|
+
type: "button",
|
|
86
|
+
class: "ui basic button",
|
|
87
|
+
data: { fui_emoji_picker_target: "preview", action: "click->fui-emoji-picker#toggle" }
|
|
88
|
+
) { (current.presence || "Pick emoji").html_safe },
|
|
89
|
+
@template.tag.div(
|
|
90
|
+
style: "display:none; position:absolute; z-index:1000;",
|
|
91
|
+
data: { fui_emoji_picker_target: "dropdown" }
|
|
92
|
+
)
|
|
93
|
+
])
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
99
|
+
# Select / Dropdown
|
|
100
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def select(attribute, choices = nil, options = {}, html_options = {}, &block)
|
|
103
|
+
use_dropdown = options.delete(:dropdown)
|
|
104
|
+
fomantic_field(attribute, options) do |attr, opts|
|
|
105
|
+
merged_html = html_options.merge(class: class_names(html_options[:class], opts.delete(:input_class)))
|
|
106
|
+
raw_select = super(attr, choices, opts, merged_html, &block)
|
|
107
|
+
|
|
108
|
+
use_dropdown ? dropdown_wrap(raw_select) : raw_select
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
113
|
+
# Check box
|
|
114
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
def check_box(attribute, options = {}, checked_value = "1", unchecked_value = "0")
|
|
117
|
+
label_text = options.delete(:label) { label_for(attribute) }
|
|
118
|
+
kind = options.delete(:kind) { :checkbox } # :checkbox | :slider | :toggle
|
|
119
|
+
|
|
120
|
+
fomantic_field(attribute, options.merge(suppress_label: true)) do |attr, opts|
|
|
121
|
+
opts.delete(:input_class)
|
|
122
|
+
checkbox_html = super(attr, opts, checked_value, unchecked_value)
|
|
123
|
+
checkbox_ui(checkbox_html, label_text, kind)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
128
|
+
# Radio button
|
|
129
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
def radio_button(attribute, value, options = {})
|
|
132
|
+
label_text = options.delete(:label) { value.to_s.humanize }
|
|
133
|
+
|
|
134
|
+
fomantic_field(attribute, options.merge(suppress_label: true)) do |attr, opts|
|
|
135
|
+
opts.delete(:input_class)
|
|
136
|
+
radio_html = super(attr, value, opts)
|
|
137
|
+
checkbox_ui(radio_html, label_text, :radio)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
142
|
+
# File field
|
|
143
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
def file_field(attribute, options = {})
|
|
146
|
+
fomantic_field(attribute, options) do |attr, opts|
|
|
147
|
+
opts.delete(:input_class)
|
|
148
|
+
super(attr, opts)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
153
|
+
# Hidden field (no wrapper)
|
|
154
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
def hidden_field(attribute, options = {})
|
|
157
|
+
super(attribute, options)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
161
|
+
# Submit button
|
|
162
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
def submit(value = nil, options = {})
|
|
165
|
+
color = options.delete(:color)
|
|
166
|
+
basic = options.delete(:basic)
|
|
167
|
+
inverted = options.delete(:inverted)
|
|
168
|
+
size = options.delete(:size)
|
|
169
|
+
icon = options.delete(:icon)
|
|
170
|
+
|
|
171
|
+
button_class = class_names(
|
|
172
|
+
"ui", "button",
|
|
173
|
+
color,
|
|
174
|
+
size,
|
|
175
|
+
("basic" if basic),
|
|
176
|
+
("inverted" if inverted),
|
|
177
|
+
options.delete(:class)
|
|
178
|
+
)
|
|
179
|
+
options[:class] = button_class
|
|
180
|
+
|
|
181
|
+
icon_html = icon ? @template.tag.i(class: "#{icon} icon") : "".html_safe
|
|
182
|
+
icon_html + super(value, options)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
186
|
+
# Label override — produces a plain <label> (called internally too)
|
|
187
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
def label(attribute, text = nil, options = {}, &block)
|
|
190
|
+
super(attribute, text, options, &block)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
194
|
+
# fields_group — wraps children in <div class="fields ...">
|
|
195
|
+
#
|
|
196
|
+
# Options:
|
|
197
|
+
# equal_width: Boolean – adds "equal width"
|
|
198
|
+
# inline: Boolean – adds "inline"
|
|
199
|
+
# count: Integer – "N fields" (evenly divided)
|
|
200
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
def fields_group(options = {}, &block)
|
|
203
|
+
equal_width = options.delete(:equal_width)
|
|
204
|
+
inline = options.delete(:inline)
|
|
205
|
+
count = options.delete(:count)
|
|
206
|
+
extra = options.delete(:class)
|
|
207
|
+
|
|
208
|
+
count_word = COLUMN_WORDS[count.to_i - 1] if count
|
|
209
|
+
|
|
210
|
+
css = class_names(
|
|
211
|
+
count_word,
|
|
212
|
+
"fields",
|
|
213
|
+
("equal width" if equal_width),
|
|
214
|
+
("inline" if inline),
|
|
215
|
+
extra
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@template.tag.div(class: css, &block)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
222
|
+
# Form-level message helpers
|
|
223
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
def error_message(header, messages = [])
|
|
226
|
+
form_message("error", header, messages)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def success_message(header, body = nil)
|
|
230
|
+
form_message("success", header, [], body)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def warning_message(header, messages = [])
|
|
234
|
+
form_message("warning", header, messages)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def info_message(header, messages = [])
|
|
238
|
+
form_message("info", header, messages)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
private
|
|
242
|
+
|
|
243
|
+
# ── Core field wrapper ─────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
def fomantic_field(attribute, options, &block)
|
|
246
|
+
required = options.delete(:required)
|
|
247
|
+
disabled = options.delete(:disabled)
|
|
248
|
+
readonly_opt = options.delete(:readonly)
|
|
249
|
+
inline = options.delete(:inline)
|
|
250
|
+
width = options.delete(:width)
|
|
251
|
+
error_msg = options.delete(:error)
|
|
252
|
+
warning_msg = options.delete(:warning)
|
|
253
|
+
hint = options.delete(:hint)
|
|
254
|
+
field_class = options.delete(:field_class)
|
|
255
|
+
suppress_label = options.delete(:suppress_label)
|
|
256
|
+
custom_label = options.delete(:label)
|
|
257
|
+
|
|
258
|
+
has_error = error_msg.present? || object_has_error?(attribute)
|
|
259
|
+
has_warning = warning_msg.present?
|
|
260
|
+
|
|
261
|
+
div_class = class_names(
|
|
262
|
+
width,
|
|
263
|
+
"field",
|
|
264
|
+
("required" if required),
|
|
265
|
+
("disabled" if disabled),
|
|
266
|
+
("read-only" if readonly_opt),
|
|
267
|
+
("inline" if inline),
|
|
268
|
+
("error" if has_error),
|
|
269
|
+
("warning" if has_warning),
|
|
270
|
+
field_class
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
label_html = unless suppress_label
|
|
274
|
+
text = custom_label.nil? ? label_for(attribute) : custom_label
|
|
275
|
+
text ? label(attribute, text) : nil
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
input_html = block.call(attribute, options)
|
|
279
|
+
|
|
280
|
+
note_html = inline_note(error_msg || (has_error && first_error(attribute)), "red")
|
|
281
|
+
note_html = inline_note(warning_msg, "orange") if note_html.blank? && has_warning
|
|
282
|
+
note_html = inline_note(hint, "grey") if note_html.blank? && hint
|
|
283
|
+
|
|
284
|
+
@template.tag.div(class: div_class) do
|
|
285
|
+
safe_join([ label_html, input_html, note_html ].compact)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# ── Checkbox / Radio UI shell ──────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
def checkbox_ui(input_html, label_text, kind)
|
|
292
|
+
modifier = { radio: "radio", slider: "slider", toggle: "toggle" }[kind]
|
|
293
|
+
css = class_names("ui", modifier, "checkbox")
|
|
294
|
+
|
|
295
|
+
@template.tag.div(class: css) do
|
|
296
|
+
safe_join([ input_html, @template.tag.label(label_text.is_a?(String) ? label_text : nil) ])
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# ── Dropdown wrapper ───────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
def dropdown_wrap(select_html)
|
|
303
|
+
@template.tag.div(class: "ui selection dropdown") do
|
|
304
|
+
safe_join([
|
|
305
|
+
@template.tag.i(class: "dropdown icon"),
|
|
306
|
+
select_html
|
|
307
|
+
])
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# ── Form-level messages ────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
def form_message(type, header, messages = [], body = nil)
|
|
314
|
+
list_html = messages.any? ? @template.tag.ul(class: "list") {
|
|
315
|
+
safe_join(messages.map { |m| @template.tag.li(m) })
|
|
316
|
+
} : nil
|
|
317
|
+
|
|
318
|
+
body_html = body ? @template.tag.p(body) : nil
|
|
319
|
+
|
|
320
|
+
@template.tag.div(class: "ui #{type} message") do
|
|
321
|
+
safe_join([
|
|
322
|
+
@template.tag.div(class: "header") { header },
|
|
323
|
+
list_html,
|
|
324
|
+
body_html
|
|
325
|
+
].compact)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# ── Inline note beneath input ──────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
def inline_note(text, color = nil)
|
|
332
|
+
return nil if text.blank?
|
|
333
|
+
|
|
334
|
+
@template.tag.small(class: class_names("ui", color, "text")) { text }
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# ── Humanise attribute name for label ─────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
def label_for(attribute)
|
|
340
|
+
attr_str = attribute.to_s.delete_suffix("_id")
|
|
341
|
+
if object && object.class.respond_to?(:human_attribute_name)
|
|
342
|
+
object.class.human_attribute_name(attr_str)
|
|
343
|
+
else
|
|
344
|
+
attr_str.humanize
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# ── ActiveModel error introspection ───────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
def object_has_error?(attribute)
|
|
351
|
+
object.respond_to?(:errors) && object.errors[attribute].any?
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def first_error(attribute)
|
|
355
|
+
object.errors[attribute].first if object.respond_to?(:errors)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# ── Safe join delegated to template ───────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
def safe_join(parts)
|
|
361
|
+
@template.safe_join(parts)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# ── class_names helper (Rails 6.1+ ships this) ────────────────────────────
|
|
365
|
+
|
|
366
|
+
def class_names(*args)
|
|
367
|
+
args.flatten.compact.reject { |v| v == false || v.to_s.strip.empty? }.join(" ")
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
372
|
+
# Usage examples (views)
|
|
373
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
# ── 1. Basic user registration form ──────────────────────────────────────────
|
|
376
|
+
#
|
|
377
|
+
# <%= form_with model: @user, builder: FomanticFormBuilder, class: "ui form" do |f| %>
|
|
378
|
+
#
|
|
379
|
+
# <%# grouped equal-width row %>
|
|
380
|
+
# <%= f.fields_group(equal_width: true) do %>
|
|
381
|
+
# <%= f.text_field :first_name, required: true %>
|
|
382
|
+
# <%= f.text_field :last_name, required: true %>
|
|
383
|
+
# <% end %>
|
|
384
|
+
#
|
|
385
|
+
# <%= f.email_field :email, required: true %>
|
|
386
|
+
# <%= f.password_field :password, required: true,
|
|
387
|
+
# hint: "Minimum 8 characters" %>
|
|
388
|
+
#
|
|
389
|
+
# <%# Native select styled by Fomantic %>
|
|
390
|
+
# <%= f.select :role, [["Admin", "admin"], ["Member", "member"]],
|
|
391
|
+
# { prompt: "Select a role" } %>
|
|
392
|
+
#
|
|
393
|
+
# <%# Fomantic dropdown widget (adds JS .dropdown() wrapper) %>
|
|
394
|
+
# <%= f.select :country, country_options,
|
|
395
|
+
# { dropdown: true, required: true } %>
|
|
396
|
+
#
|
|
397
|
+
# <%= f.check_box :terms,
|
|
398
|
+
# label: "I agree to the Terms and Conditions",
|
|
399
|
+
# required: true %>
|
|
400
|
+
#
|
|
401
|
+
# <%# Error/success messages from model %>
|
|
402
|
+
# <%= f.error_message "We had some issues",
|
|
403
|
+
# @user.errors.full_messages if @user.errors.any? %>
|
|
404
|
+
#
|
|
405
|
+
# <%= f.submit "Sign up", color: "green" %>
|
|
406
|
+
# <% end %>
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ── 2. Inline field (label beside input) ─────────────────────────────────────
|
|
410
|
+
#
|
|
411
|
+
# <%= f.text_field :phone, label: "Phone Number", inline: true,
|
|
412
|
+
# width: "eight" %>
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# ── 3. Width-constrained fields ───────────────────────────────────────────────
|
|
416
|
+
#
|
|
417
|
+
# <%= f.fields_group do %>
|
|
418
|
+
# <%= f.text_field :first_name, width: "six" %>
|
|
419
|
+
# <%= f.text_field :middle, width: "three" %>
|
|
420
|
+
# <%= f.text_field :last_name, width: "seven" %>
|
|
421
|
+
# <% end %>
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# ── 4. Checkbox kinds ─────────────────────────────────────────────────────────
|
|
425
|
+
#
|
|
426
|
+
# <%= f.check_box :notifications, label: "Enable notifications", kind: :toggle %>
|
|
427
|
+
# <%= f.check_box :public, label: "Publicly visible", kind: :slider %>
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# ── 5. Radio group ────────────────────────────────────────────────────────────
|
|
431
|
+
#
|
|
432
|
+
# <%= f.fields_group do %>
|
|
433
|
+
# <%= f.radio_button :plan, "basic", label: "Basic" %>
|
|
434
|
+
# <%= f.radio_button :plan, "pro", label: "Pro" %>
|
|
435
|
+
# <%= f.radio_button :plan, "enterprise", label: "Enterprise" %>
|
|
436
|
+
# <% end %>
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ── 6. Textarea ───────────────────────────────────────────────────────────────
|
|
440
|
+
#
|
|
441
|
+
# <%= f.text_area :bio, rows: 4 %>
|
|
442
|
+
# <%= f.text_area :description, rows: 2, transparent: true %>
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# ── 7. Form-level state messages ──────────────────────────────────────────────
|
|
446
|
+
#
|
|
447
|
+
# <%= f.error_message "Action Forbidden", ["Email already registered"] %>
|
|
448
|
+
# <%= f.success_message "All done!", "Your profile has been updated." %>
|
|
449
|
+
# <%= f.warning_message "Heads up", ["Please verify your email"] %>
|
|
450
|
+
# <%= f.info_message "Password rules", ["Must be at least 8 characters"] %>
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# ── 8. Submit variations ──────────────────────────────────────────────────────
|
|
454
|
+
#
|
|
455
|
+
# <%= f.submit "Save", color: "blue" %>
|
|
456
|
+
# <%= f.submit "Delete", color: "red", basic: true %>
|
|
457
|
+
# <%= f.submit "Go", color: "green", size: "large", icon: "checkmark" %>
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
# ── 9. Opt-in per form (when default builder is not set) ──────────────────────
|
|
461
|
+
#
|
|
462
|
+
# <%= form_with model: @post, builder: FomanticFormBuilder, class: "ui form" do |f| %>
|
|
463
|
+
# ...
|
|
464
|
+
# <% end %>
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
468
|
+
# JavaScript initialisation (application.js or a Stimulus controller)
|
|
469
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
470
|
+
#
|
|
471
|
+
# // Initialise all Fomantic dropdowns on the page
|
|
472
|
+
# document.addEventListener("DOMContentLoaded", () => {
|
|
473
|
+
# $(".ui.dropdown").dropdown();
|
|
474
|
+
# $(".ui.checkbox").checkbox();
|
|
475
|
+
# $(".ui.calendar").calendar({ type: "date" });
|
|
476
|
+
# });
|
|
477
|
+
#
|
|
478
|
+
# // Or with a Stimulus controller:
|
|
479
|
+
# //
|
|
480
|
+
# // import { Controller } from "@hotwired/stimulus"
|
|
481
|
+
# // export default class extends Controller {
|
|
482
|
+
# // connect() {
|
|
483
|
+
# // $(this.element).find(".ui.dropdown").dropdown()
|
|
484
|
+
# // $(this.element).find(".ui.checkbox").checkbox()
|
|
485
|
+
# // }
|
|
486
|
+
# // }
|
|
487
|
+
end
|
data/app/lib/component.rb
CHANGED
|
@@ -19,8 +19,10 @@ class Component
|
|
|
19
19
|
class_attribute :slot_names, default: []
|
|
20
20
|
|
|
21
21
|
def initialize(**kwargs)
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
known = self.class.attribute_names.map(&:to_sym).to_set
|
|
23
|
+
attrs = kwargs.select { |k, _| known.include?(k) }
|
|
24
|
+
@html_options = kwargs.except(*attrs.keys)
|
|
25
|
+
super(**attrs)
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def self.default(**overrides)
|
data/lib/ui/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-active-ui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- nathan
|
|
@@ -37,6 +37,7 @@ executables: []
|
|
|
37
37
|
extensions: []
|
|
38
38
|
extra_rdoc_files: []
|
|
39
39
|
files:
|
|
40
|
+
- README.md
|
|
40
41
|
- Rakefile
|
|
41
42
|
- app/assets/datatables.css
|
|
42
43
|
- app/assets/stylesheets.css
|
|
@@ -125,6 +126,7 @@ files:
|
|
|
125
126
|
- app/controllers/ui/examples_controller.rb
|
|
126
127
|
- app/helpers/component_helper.rb
|
|
127
128
|
- app/helpers/fui_helper.rb
|
|
129
|
+
- app/helpers/ui/fomantic_form_builder.rb
|
|
128
130
|
- app/javascript/accordion.js
|
|
129
131
|
- app/javascript/accordion.min.js
|
|
130
132
|
- app/javascript/api.js
|