phlexi-field 0.1.0 → 0.2.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/.standard.yml +2 -0
- data/Appraisals +12 -3
- data/README.md +260 -121
- data/Rakefile +29 -0
- data/gemfiles/default.gemfile +7 -0
- data/gemfiles/default.gemfile.lock +201 -91
- data/gemfiles/rails_7.1.gemfile +13 -0
- data/gemfiles/rails_7.1.gemfile.lock +314 -0
- data/gemfiles/rails_8.gemfile +13 -0
- data/gemfiles/rails_8.gemfile.lock +309 -0
- data/lib/phlexi/field/common/tokens.rb +58 -0
- data/lib/phlexi/field/components/base.rb +3 -3
- data/lib/phlexi/field/structure/namespace_collection.rb +4 -0
- data/lib/phlexi/field/version.rb +1 -1
- metadata +16 -9
- data/gemfiles/rails_7.gemfile +0 -8
- data/gemfiles/rails_7.gemfile.lock +0 -282
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23043c21dcd13b30389b9abde3064b5ad3cd6e3a3d113632812b08a81146e50d
|
4
|
+
data.tar.gz: 807e164c121afec7d2cfc376f1d09b9bed972d3adf8cfef73774ee95b48cd3e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb81612c85d3d0bca6391940735150391e53c6a40208791142ba97dff7bb35257a7bfa02bf5d908025758e2b6f62c20635299170c9ebfbabc9b52b2ae19d00e9
|
7
|
+
data.tar.gz: 1a0c409e68029ba5b702ad3642f7e2f8720ccbfcbe5ea25564a72e3759ac91dabb46fd8b7588fb3bb1b7e0c5ee1c3c2a9e11fa56003afe2852a0f0d12a5aa7c9
|
data/.standard.yml
ADDED
data/Appraisals
CHANGED
@@ -1,8 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
appraise "default" do
|
2
|
-
#
|
4
|
+
# Default dependencies
|
3
5
|
end
|
4
6
|
|
5
|
-
appraise "rails-7" do
|
6
|
-
gem "rails", "~> 7.1.3", ">= 7.1.3.4"
|
7
|
+
appraise "rails-7.1" do
|
8
|
+
gem "rails", "~> 7.1.3", ">= 7.1.3.4", "< 8.0.0"
|
7
9
|
gem "sqlite3", "~> 1.4"
|
10
|
+
gem "phlex-rails", "~> 2.2"
|
11
|
+
end
|
12
|
+
|
13
|
+
appraise "rails-8" do
|
14
|
+
gem "rails", "~> 8.0.0"
|
15
|
+
gem "sqlite3", "~> 2.1"
|
16
|
+
gem "phlex-rails", "~> 2.2"
|
8
17
|
end
|
data/README.md
CHANGED
@@ -1,195 +1,334 @@
|
|
1
|
-
# Phlexi::
|
1
|
+
# Phlexi::Field
|
2
2
|
|
3
|
-
|
3
|
+
[](https://badge.fury.io/rb/phlexi-field)
|
4
|
+
[](https://github.com/radioactive-labs/phlexi-field/actions/workflows/main.yml)
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
4
6
|
|
5
|
-
|
7
|
+
Phlexi::Field is a field component framework for Ruby applications that provides a unified field abstraction across forms, displays, and tables. Built on a namespace-based architecture for handling complex object relationships.
|
8
|
+
It's designed to work with [Phlex](https://github.com/phlex-ruby/phlex), a framework for building view components in Ruby.
|
6
9
|
|
7
|
-
##
|
10
|
+
## Design Philosophy & Purpose
|
8
11
|
|
9
|
-
|
10
|
-
- Automatic field type and attribute inference based on model attributes
|
11
|
-
- Built-in support for validations and error handling
|
12
|
-
- Flexible form structure with support for nested attributes
|
13
|
-
- Works with Phlex or erb views
|
14
|
-
- Extract input from parameters that match your form definition. No need to strong paramters.
|
15
|
-
- Rails compatible form inputs
|
12
|
+
Phlexi::Field serves as the foundation for field rendering across multiple contexts in web applications. Unlike traditional form builders that focus solely on forms, Phlexi::Field creates a unified abstraction for working with fields that can be used for:
|
16
13
|
|
14
|
+
- Form inputs
|
15
|
+
- Read-only displays
|
16
|
+
- Table columns
|
17
|
+
- Any other UI context where object fields are presented
|
18
|
+
|
19
|
+
It is designed around a **namespace-based architecture** that creates a hierarchical tree structure for complex object graphs, handling nested relationships and collections gracefully.
|
17
20
|
|
18
21
|
## Installation
|
19
22
|
|
20
23
|
Add this line to your application's Gemfile:
|
21
24
|
|
22
25
|
```ruby
|
23
|
-
gem 'phlexi-
|
26
|
+
gem 'phlexi-field'
|
24
27
|
```
|
25
28
|
|
26
29
|
And then execute:
|
27
30
|
|
28
|
-
```
|
31
|
+
```bash
|
29
32
|
$ bundle install
|
30
33
|
```
|
31
34
|
|
32
35
|
Or install it yourself as:
|
33
36
|
|
34
|
-
```
|
35
|
-
$ gem install phlexi-
|
37
|
+
```bash
|
38
|
+
$ gem install phlexi-field
|
36
39
|
```
|
37
40
|
|
38
|
-
##
|
41
|
+
## Requirements
|
42
|
+
|
43
|
+
- Ruby >= 3.2.2
|
44
|
+
- Phlex ~> 2.0
|
45
|
+
- ActiveSupport >= 7.1
|
39
46
|
|
40
|
-
|
47
|
+
## Core Architecture
|
41
48
|
|
42
|
-
###
|
49
|
+
### Namespace Pattern
|
50
|
+
|
51
|
+
The foundation of Phlexi::Field is the `Namespace` concept:
|
52
|
+
|
53
|
+
1. A Namespace maps an object (like an ActiveRecord model) to a key
|
54
|
+
2. Fields are created within a namespace
|
55
|
+
3. Namespaces can be nested to represent complex object relationships
|
43
56
|
|
44
57
|
```ruby
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
58
|
+
# Create a root namespace for a user object
|
59
|
+
namespace = Phlexi::Field::Structure::Namespace.new(
|
60
|
+
:user, # The key/name
|
61
|
+
parent: nil, # Root namespace has no parent
|
62
|
+
builder_klass: Phlexi::Field::Builder, # Field builder class
|
63
|
+
object: @user # The data object
|
64
|
+
)
|
65
|
+
|
66
|
+
# Create a field within the namespace
|
67
|
+
email_field = namespace.field(:email)
|
68
|
+
|
69
|
+
# Access field properties
|
70
|
+
email_field.dom.id # => "user_email"
|
71
|
+
email_field.dom.name # => "user[email]"
|
72
|
+
email_field.value # => The email value from @user
|
73
|
+
```
|
50
74
|
|
51
|
-
|
52
|
-
render email.label_tag
|
53
|
-
render email.input_tag
|
54
|
-
end
|
75
|
+
### Nested Objects and Collections
|
55
76
|
|
56
|
-
|
57
|
-
address.field(:street) do |street|
|
58
|
-
render street.label_tag
|
59
|
-
render street.input_tag
|
60
|
-
end
|
77
|
+
One of Phlexi::Field's strengths is handling complex object graphs:
|
61
78
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
79
|
+
```ruby
|
80
|
+
# For a has_one relationship
|
81
|
+
namespace.nest_one(:profile) do |profile|
|
82
|
+
first_name = profile.field(:first_name)
|
83
|
+
# DOM properties: id="user_profile_first_name", name="user[profile][first_name]"
|
84
|
+
end
|
67
85
|
|
68
|
-
|
86
|
+
# For a has_many relationship
|
87
|
+
namespace.nest_many(:addresses) do |address_builder|
|
88
|
+
street = address_builder.field(:street)
|
89
|
+
# DOM properties: id="user_addresses_street", name="user[addresses][][street]"
|
69
90
|
end
|
70
91
|
```
|
71
92
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
93
|
+
### Field Builders and Components
|
94
|
+
|
95
|
+
Phlexi::Field separates field creation from rendering:
|
96
|
+
|
97
|
+
- `Builder` classes handle field creation, value access, and configuration
|
98
|
+
- `Component` classes handle presentation and rendering
|
99
|
+
|
100
|
+
This separation allows the same field structure to be rendered differently in various contexts.
|
101
|
+
|
102
|
+
## Field Configuration Options
|
103
|
+
|
104
|
+
Phlexi::Field provides a rich set of options for configuring how fields are displayed and behave:
|
105
|
+
|
106
|
+
### Labels
|
107
|
+
|
108
|
+
Labels can be explicitly set or automatically generated from the object's attribute name:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
# Custom label
|
112
|
+
field = namespace.field(:email, label: "Email Address")
|
113
|
+
|
114
|
+
# Get the current label (automatically falls back to humanized attribute name)
|
115
|
+
field.label # => "Email Address" or "Email" if not explicitly set
|
116
|
+
|
117
|
+
# Rails integration: Uses human_attribute_name if available
|
118
|
+
# user.class.human_attribute_name(:email) => "Email Address"
|
119
|
+
```
|
120
|
+
|
121
|
+
### Hints and Descriptions
|
84
122
|
|
85
|
-
|
123
|
+
Hints provide contextual help for users, while descriptions offer more detailed explanations:
|
86
124
|
|
87
125
|
```ruby
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
126
|
+
# Setting a hint for a field
|
127
|
+
field = namespace.field(:password, hint: "Must be at least 8 characters")
|
128
|
+
field.hint # => "Must be at least 8 characters"
|
129
|
+
field.has_hint? # => true
|
130
|
+
|
131
|
+
# Setting a description
|
132
|
+
field = namespace.field(:terms_accepted,
|
133
|
+
description: "By accepting our terms, you agree to our privacy policy.")
|
134
|
+
field.description # => "By accepting our terms, you agree to our privacy policy."
|
135
|
+
field.has_description? # => true
|
136
|
+
```
|
94
137
|
|
95
|
-
|
96
|
-
render email.label_tag
|
97
|
-
render email.input_tag
|
98
|
-
end
|
138
|
+
### Placeholders
|
99
139
|
|
100
|
-
|
101
|
-
address.field(:street) do |street|
|
102
|
-
render street.label_tag
|
103
|
-
render street.input_tag
|
104
|
-
end
|
140
|
+
Text placeholders can be set for input fields:
|
105
141
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
end
|
142
|
+
```ruby
|
143
|
+
field = namespace.field(:email, placeholder: "john@example.com")
|
144
|
+
field.placeholder # => "john@example.com"
|
145
|
+
```
|
111
146
|
|
112
|
-
|
113
|
-
|
114
|
-
|
147
|
+
### Type Inference
|
148
|
+
|
149
|
+
Phlexi::Field automatically infers the appropriate field type based on multiple signals:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
# Based on attribute/column type in ActiveRecord/ActiveModel
|
153
|
+
# email_field.inferred_field_type # => :string, :integer, :boolean, etc.
|
154
|
+
|
155
|
+
# For string fields, it can also infer more specific types
|
156
|
+
# email_field.inferred_string_field_type # => :email, :password, :url, etc.
|
157
|
+
```
|
158
|
+
|
159
|
+
The type inference algorithm considers:
|
160
|
+
1. ActiveRecord column types
|
161
|
+
2. ActiveModel attribute types
|
162
|
+
3. Value type inspection
|
163
|
+
4. Field name conventions (email, password, url, etc.)
|
164
|
+
5. Validation rules (email format, numericality, etc.)
|
165
|
+
|
166
|
+
### Validators
|
167
|
+
|
168
|
+
When using ActiveModel validations, Phlexi::Field can access them:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
# Check if a field is required based on presence validators
|
172
|
+
field.required? # => true/false based on presence validators
|
115
173
|
|
174
|
+
# Access the validation errors
|
175
|
+
field.errors # => ["can't be blank", etc.]
|
176
|
+
```
|
116
177
|
|
117
|
-
|
118
|
-
form = UserForm.new(User.new)
|
178
|
+
### Multiple Options
|
119
179
|
|
120
|
-
|
121
|
-
render form
|
180
|
+
For fields that support multiple selections:
|
122
181
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
email: "brad@pitt.com",
|
127
|
-
address: {
|
128
|
-
street: "Plumbago",
|
129
|
-
city: "Accra",
|
130
|
-
}
|
131
|
-
})
|
182
|
+
```ruby
|
183
|
+
field = namespace.field(:categories, multiple: true)
|
184
|
+
field.multiple? # => true
|
132
185
|
```
|
133
186
|
|
134
|
-
##
|
187
|
+
## Usage
|
188
|
+
|
189
|
+
Phlexi::Field provides a foundation for building components that handle object fields such as forms and other UI components. It handles DOM structure, field values, configuration of labels, descriptions, hints, and more.
|
135
190
|
|
136
|
-
|
191
|
+
It serves as a foundation for [Phlexi::Form](https://github.com/radioactive-labs/phlexi-form), [Phlexi::Display](https://github.com/radioactive-labs/phlexi-display) and [Phlexi::Table](https://github.com/radioactive-labs/phlexi-table).
|
137
192
|
|
138
|
-
|
193
|
+
### Basic Example
|
139
194
|
|
140
195
|
```ruby
|
141
|
-
class
|
196
|
+
class UserForm < Phlex::HTML
|
197
|
+
def initialize(user)
|
198
|
+
super()
|
199
|
+
@user = user
|
200
|
+
end
|
201
|
+
|
142
202
|
def view_template
|
143
|
-
|
144
|
-
|
145
|
-
|
203
|
+
namespace = Phlexi::Field::Structure::Namespace.new(
|
204
|
+
:user,
|
205
|
+
parent: nil,
|
206
|
+
builder_klass: Phlexi::Field::Builder,
|
207
|
+
object: @user
|
208
|
+
)
|
209
|
+
|
210
|
+
form(action: "/users", method: "post") do
|
211
|
+
# Create a name field
|
212
|
+
name_field = namespace.field(:name)
|
213
|
+
label(for: name_field.dom.id) { name_field.label }
|
214
|
+
input(
|
215
|
+
id: name_field.dom.id,
|
216
|
+
name: name_field.dom.name,
|
217
|
+
value: name_field.value,
|
218
|
+
type: "text"
|
219
|
+
)
|
220
|
+
|
221
|
+
# Create an email field
|
222
|
+
email_field = namespace.field(:email)
|
223
|
+
label(for: email_field.dom.id) { email_field.label }
|
224
|
+
input(
|
225
|
+
id: email_field.dom.id,
|
226
|
+
name: email_field.dom.name,
|
227
|
+
value: email_field.value,
|
228
|
+
type: "email"
|
229
|
+
)
|
230
|
+
|
231
|
+
# Submit button
|
232
|
+
button(type: "submit") { "Submit" }
|
146
233
|
end
|
147
234
|
end
|
148
235
|
end
|
149
236
|
|
150
|
-
# Usage in
|
151
|
-
|
152
|
-
|
237
|
+
# Usage in a controller
|
238
|
+
def new
|
239
|
+
user = User.new
|
240
|
+
render UserForm.new(user)
|
153
241
|
end
|
154
242
|
```
|
155
243
|
|
156
|
-
###
|
244
|
+
### Nested Forms
|
157
245
|
|
158
|
-
Phlexi::
|
246
|
+
Phlexi::Field supports nested forms for complex data structures:
|
159
247
|
|
160
248
|
```ruby
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
def default_theme
|
166
|
-
{
|
167
|
-
input: "border rounded px-2 py-1",
|
168
|
-
label: "font-bold text-gray-700",
|
169
|
-
# Add more theme options here
|
170
|
-
}
|
171
|
-
end
|
172
|
-
end
|
249
|
+
# For a has_one relationship
|
250
|
+
namespace.nest_one(:profile) do |profile|
|
251
|
+
profile.field(:first_name) # => Creates a field for user[profile][first_name]
|
173
252
|
end
|
253
|
+
|
254
|
+
# For a has_many relationship
|
255
|
+
namespace.nest_many(:addresses) do |address_builder|
|
256
|
+
address_field = address_builder.field(:id)
|
257
|
+
# Creates fields like user[addresses][][id]
|
258
|
+
end
|
259
|
+
```
|
260
|
+
|
261
|
+
### Field Options
|
262
|
+
|
263
|
+
Fields support various options for customization:
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
# Custom label
|
267
|
+
field = namespace.field(:email, label: "Email Address")
|
268
|
+
|
269
|
+
# Required fields
|
270
|
+
field = namespace.field(:email, required: true)
|
271
|
+
|
272
|
+
# Placeholder text
|
273
|
+
field = namespace.field(:email, placeholder: "Enter your email")
|
274
|
+
|
275
|
+
# Description/hint text
|
276
|
+
field = namespace.field(:password, hint: "Must be at least 8 characters")
|
174
277
|
```
|
175
278
|
|
176
|
-
|
279
|
+
## Components
|
177
280
|
|
178
|
-
|
281
|
+
Phlexi::Field comes with a base component architecture that you can extend:
|
179
282
|
|
180
283
|
```ruby
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
284
|
+
class MyInputComponent < Phlexi::Field::Components::Base
|
285
|
+
def view_template
|
286
|
+
input(
|
287
|
+
id: field.dom.id,
|
288
|
+
name: field.dom.name,
|
289
|
+
value: field.value,
|
290
|
+
type: "text",
|
291
|
+
**attributes
|
292
|
+
)
|
293
|
+
end
|
187
294
|
end
|
188
|
-
|
295
|
+
|
296
|
+
# Usage
|
297
|
+
my_component = MyInputComponent.new(field, class: "custom-input")
|
298
|
+
render my_component
|
299
|
+
```
|
300
|
+
|
301
|
+
## Comparison with Other Libraries
|
302
|
+
|
303
|
+
Phlexi::Field differs from traditional Rails form helpers and gems like Simple Form in several key ways:
|
304
|
+
|
305
|
+
1. **Unified Field API**: Creates a consistent interface for fields across forms, displays, and tables
|
306
|
+
2. **Component-Based**: Built on Phlex's component system rather than Rails' helper-based approach
|
307
|
+
3. **Separation of Concerns**: Cleanly separates field structure (namespace), data handling (builder), and presentation (components)
|
308
|
+
4. **True Object Orientation**: Works with complete objects and their relationships rather than flat attribute lists
|
309
|
+
|
310
|
+
## Integration with Rails
|
311
|
+
|
312
|
+
While Phlexi::Field works standalone with Phlex, it has first-class support for Rails:
|
313
|
+
|
314
|
+
```ruby
|
315
|
+
# In your Gemfile
|
316
|
+
gem 'phlexi-field'
|
317
|
+
gem 'phlex-rails'
|
318
|
+
```
|
319
|
+
|
320
|
+
The library automatically uses Rails conventions when available:
|
321
|
+
- Humanized attribute names via `human_attribute_name`
|
322
|
+
- Association detection via `reflect_on_association`
|
323
|
+
- Primary key handling
|
324
|
+
|
325
|
+
## Development
|
326
|
+
|
327
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
189
328
|
|
190
329
|
## Contributing
|
191
330
|
|
192
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/radioactive-labs/phlexi-
|
331
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/radioactive-labs/phlexi-field.
|
193
332
|
|
194
333
|
## License
|
195
334
|
|
data/Rakefile
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
require "rake/testtask"
|
5
5
|
require "standard/rake"
|
6
|
+
require "appraisal"
|
6
7
|
|
7
8
|
task default: %i[test standard]
|
8
9
|
|
@@ -12,3 +13,31 @@ Rake::TestTask.new do |t|
|
|
12
13
|
t.test_files = FileList["test/**/*_test.rb"]
|
13
14
|
t.verbose = true
|
14
15
|
end
|
16
|
+
|
17
|
+
Rake::TestTask.new(:test) do |t|
|
18
|
+
t.libs << "test"
|
19
|
+
t.libs << "lib"
|
20
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
21
|
+
end
|
22
|
+
|
23
|
+
namespace :test do
|
24
|
+
desc "Run core tests that don't depend on Rails"
|
25
|
+
Rake::TestTask.new(:core) do |t|
|
26
|
+
t.libs << "test"
|
27
|
+
t.libs << "lib"
|
28
|
+
t.test_files = FileList["test/phlexi/field/**/*_test.rb"].exclude("test/phlexi/rails/**/*_test.rb")
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "Run Rails integration tests"
|
32
|
+
Rake::TestTask.new(:rails) do |t|
|
33
|
+
t.libs << "test"
|
34
|
+
t.libs << "lib"
|
35
|
+
t.test_files = FileList["test/phlexi/rails/**/*_test.rb"]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "Run tests across all appraisals"
|
40
|
+
task :test_all do
|
41
|
+
sh "bundle exec appraisal install"
|
42
|
+
sh "bundle exec appraisal test"
|
43
|
+
end
|