phlexi-field 0.0.1

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.
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Field
5
+ module Structure
6
+ # Generates DOM IDs, names, etc. for a Field, Namespace, or Node based on
7
+ # norms that were established by Rails. These can be used outside of or Rails in
8
+ # other Ruby web frameworks since it has no dependencies on Rails.
9
+ class DOM
10
+ def initialize(field:)
11
+ @field = field
12
+ end
13
+
14
+ # Converts the value of the field to a String, which is required to work
15
+ # with Phlex. Assumes that `Object#to_s` emits a format suitable for display.
16
+ def value
17
+ @field.value.to_s
18
+ end
19
+
20
+ # Walks from the current node to the parent node, grabs the names, and separates
21
+ # them with a `_` for a DOM ID.
22
+ def id
23
+ @id ||= begin
24
+ root, *rest = lineage
25
+ root_key = root.respond_to?(:dom_id) ? root.dom_id : root.key
26
+ rest.map(&:key).unshift(root_key).join("_")
27
+ end
28
+ end
29
+
30
+ # The `name` attribute of a node, which is influenced by Rails.
31
+ # All node names, except the parent node, are wrapped in a `[]` and collections
32
+ # are left empty. For example, `user[addresses][][street]` would be created for a form with
33
+ # data shaped like `{user: {addresses: [{street: "Sesame Street"}]}}`.
34
+ def name
35
+ @name ||= begin
36
+ root, *names = keys
37
+ names.map { |name| "[#{name}]" }.unshift(root).join
38
+ end
39
+ end
40
+
41
+ # One-liner way of walking from the current node all the way up to the parent.
42
+ def lineage
43
+ @lineage ||= Enumerator.produce(@field, &:parent).take_while(&:itself).reverse
44
+ end
45
+
46
+ # Emit the id, name, and value in an HTML tag-ish that doesnt have an element.
47
+ def inspect
48
+ "<#{self.class.name} id=#{id.inspect} name=#{name.inspect} value=#{value.inspect}/>"
49
+ end
50
+
51
+ private
52
+
53
+ def keys
54
+ @keys ||= lineage.map do |node|
55
+ # If the parent of a field is a field, the name should be nil.
56
+ node.key unless node.parent.is_a? FieldBuilder
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Field
5
+ module Structure
6
+ class FieldCollection
7
+ include Enumerable
8
+
9
+ class Builder
10
+ attr_reader :key, :index
11
+
12
+ def initialize(key, field, index)
13
+ @key = key.to_s
14
+ @field = field
15
+ @index = index
16
+ end
17
+
18
+ def field(**)
19
+ @field.class.new(key, **, parent: @field).tap do |field|
20
+ yield field if block_given?
21
+ end
22
+ end
23
+ end
24
+
25
+ def initialize(field:, collection:, &)
26
+ @field = field
27
+ @collection = build_collection(collection)
28
+ each(&) if block_given?
29
+ end
30
+
31
+ def each(&)
32
+ @collection.each.with_index do |item, index|
33
+ yield self.class::Builder.new(item, @field, index)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def build_collection(collection)
40
+ collection
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Field
5
+ module Structure
6
+ # A Namespace maps an object to values, but doesn't actually have a value itself. For
7
+ # example, a `User` object or ActiveRecord model could be passed into the `:user` namespace.
8
+ #
9
+ # To access single values on a Namespace, #field can be used.
10
+ #
11
+ # To access nested objects within a namespace, two methods are available:
12
+ #
13
+ # 1. #nest_one: Used for single nested objects, such as if a `User belongs_to :profile` in
14
+ # ActiveRecord. This method returns another Namespace object.
15
+ #
16
+ # 2. #nest_many: Used for collections of nested objects, such as if a `User has_many :addresses` in
17
+ # ActiveRecord. This method returns a NamespaceCollection object.
18
+ class Namespace < Structure::Node
19
+ include Enumerable
20
+
21
+ attr_reader :builder_klass, :object
22
+
23
+ def initialize(key, parent:, builder_klass:, object: nil)
24
+ super(key, parent: parent)
25
+ @builder_klass = builder_klass
26
+ @object = object
27
+ @children = {}
28
+ yield self if block_given?
29
+ end
30
+
31
+ def field(key, **attributes)
32
+ create_child(key, attributes.delete(:builder_klass) || builder_klass, object: object, **attributes).tap do |field|
33
+ yield field if block_given?
34
+ end
35
+ end
36
+
37
+ # Creates a `Namespace` child instance with the parent set to the current instance, adds to
38
+ # the `@children` Hash to ensure duplicate child namespaces aren't created, then calls the
39
+ # method on the `@object` to get the child object to pass into that namespace.
40
+ #
41
+ # For example, if a `User#permission` returns a `Permission` object, we could map that to a
42
+ # form like this:
43
+ #
44
+ # ```ruby
45
+ # Phlexi::Form(User.new, as: :user) do
46
+ # nest_one :profile do |profile|
47
+ # render profile.field(:gender).input_tag
48
+ # end
49
+ # end
50
+ # ```
51
+ def nest_one(key, object: nil, &)
52
+ object ||= object_value_for(key: key)
53
+ create_child(key, self.class, object:, builder_klass:, &)
54
+ end
55
+
56
+ # Wraps an array of objects in Namespace classes. For example, if `User#addresses` returns
57
+ # an enumerable or array of `Address` classes:
58
+ #
59
+ # ```ruby
60
+ # Phlexi::Form(User.new) do
61
+ # render field(:email).input_tag
62
+ # render field(:name).input_tag
63
+ # nest_many :addresses do |address|
64
+ # render address.field(:street).input_tag
65
+ # render address.field(:state).input_tag
66
+ # render address.field(:zip).input_tag
67
+ # end
68
+ # end
69
+ # ```
70
+ # The object within the block is a `Namespace` object that maps each object within the enumerable
71
+ # to another `Namespace` or `Field`.
72
+ def nest_many(key, collection: nil, &)
73
+ collection ||= Array(object_value_for(key: key))
74
+ create_child(key, NamespaceCollection, collection:, &)
75
+ end
76
+
77
+ # Iterates through the children of the current namespace, which could be `Namespace` or `Field`
78
+ # objects.
79
+ def each(&)
80
+ @children.values.each(&)
81
+ end
82
+
83
+ def dom_id
84
+ @dom_id ||= begin
85
+ id = if object.nil?
86
+ nil
87
+ elsif object.class.respond_to?(:primary_key)
88
+ object.public_send(object.class.primary_key) || :new
89
+ elsif object.respond_to?(:id)
90
+ object.id || :new
91
+ end
92
+ [key, id].compact.join("_").underscore
93
+ end
94
+ end
95
+
96
+ # Creates a root Namespace.
97
+ def self.root(*, builder_klass:, **, &)
98
+ new(*, parent: nil, builder_klass:, **, &)
99
+ end
100
+
101
+ protected
102
+
103
+ # Calls the corresponding method on the object for the `key` name, if it exists. For example
104
+ # if the `key` is `email` on `User`, this method would call `User#email` if the method is
105
+ # present.
106
+ #
107
+ # This method could be overwritten if the mapping between the `@object` and `key` name is not
108
+ # a method call. For example, a `Hash` would be accessed via `user[:email]` instead of `user.send(:email)`
109
+ def object_value_for(key:)
110
+ return @object.send(key) if @object.respond_to?(key)
111
+ @object.fetch(key) if @object.is_a?(Hash)
112
+ end
113
+
114
+ private
115
+
116
+ # Checks if the child exists. If it does then it returns that. If it doesn't, it will
117
+ # build the child.
118
+ def create_child(key, child_class, **kwargs, &block)
119
+ @children.fetch(key) { @children[key] = child_class.new(key, parent: self, **kwargs, &block) }
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Field
5
+ module Structure
6
+ class NamespaceCollection < Node
7
+ include Enumerable
8
+
9
+ def initialize(key, parent:, collection: nil, &block)
10
+ raise ArgumentError, "block is required" unless block.present?
11
+
12
+ super(key, parent: parent)
13
+
14
+ @collection = collection
15
+ @block = block
16
+ each(&block)
17
+ end
18
+
19
+ private
20
+
21
+ def each(&)
22
+ namespaces.each(&)
23
+ end
24
+
25
+ # Builds and memoizes namespaces for the collection.
26
+ #
27
+ # @return [Array<Namespace>] An array of namespace objects.
28
+ def namespaces
29
+ @namespaces ||= @collection.map.with_index do |object, key|
30
+ build_namespace(key, object: object)
31
+ end
32
+ end
33
+
34
+ def build_namespace(index, **)
35
+ parent.class.new(index, parent: self, builder_klass: parent.builder_klass, **)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Field
5
+ module Structure
6
+ # Superclass for Namespace and Field classes. Represents a node in the field tree structure.
7
+ #
8
+ # @attr_reader [Symbol] key The node's key
9
+ # @attr_reader [Node, nil] parent The node's parent in the tree structure
10
+ class Node
11
+ attr_reader :key, :parent
12
+
13
+ # Initializes a new Node instance.
14
+ #
15
+ # @param key [Symbol, String] The key for the node
16
+ # @param parent [Node, nil] The parent node
17
+ def initialize(key, parent:)
18
+ @key = :"#{key}"
19
+ @parent = parent
20
+ end
21
+
22
+ def inspect
23
+ "<#{self.class.name} key=#{key.inspect} parent=#{id.inspect} />"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,61 @@
1
+ require "fiber/local"
2
+
3
+ module Phlexi
4
+ module Field
5
+ module Theme
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ extend Fiber::Local
10
+ end
11
+
12
+ # Retrieves the theme hash
13
+ #
14
+ # This method returns a hash containing theme definitions for various display components.
15
+ # If a theme has been explicitly set in the options, it returns that. Otherwise, it
16
+ # initializes and returns a default theme.
17
+ #
18
+ # The theme hash defines CSS classes or references to other theme keys for different
19
+ # components, allowing for a flexible and inheritance-based theming system.
20
+ #
21
+ # @return [Hash] A hash containing theme definitions for display components
22
+ #
23
+ # @example Accessing the theme
24
+ # theme[:text]
25
+ # # => "text-gray-700 text-sm"
26
+ #
27
+ # @example Theme inheritance
28
+ # theme[:email] # Returns :text, indicating email inherits text's theme
29
+ def theme
30
+ raise NotImplementedError, "#{self.class} must implement #theme"
31
+ end
32
+
33
+ # Recursively resolves the theme for a given property, handling nested symbol references
34
+ #
35
+ # @param property [Symbol, String] The theme property to resolve
36
+ # @param visited [Set] Set of already visited properties to prevent infinite recursion
37
+ # @return [String, nil] The resolved theme value or nil if not found
38
+ #
39
+ # @example Basic usage
40
+ # # Assuming the theme is: { text: "text-gray-700", email: :text }
41
+ # themed(:text)
42
+ # # => "text-gray-700 text-sm"
43
+ #
44
+ # @example Cascading themes
45
+ # # Assuming the theme is: { text: "text-gray-700", email: :text }
46
+ # resolve_theme(:email)
47
+ # # => "text-gray-700"
48
+ def resolve_theme(property, visited = Set.new)
49
+ return nil if !property.present? || visited.include?(property)
50
+ visited.add(property)
51
+
52
+ result = theme[property]
53
+ if result.is_a?(Symbol)
54
+ resolve_theme(result, visited)
55
+ else
56
+ result
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Field
5
+ VERSION = "0.0.1"
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ require "phlex"
5
+ require "active_support/core_ext/object/blank"
6
+
7
+ module Phlexi
8
+ module Field
9
+ Loader = Zeitwerk::Loader.new.tap do |loader|
10
+ loader.tag = File.basename(__FILE__, ".rb")
11
+ loader.inflector.inflect(
12
+ "phlexi-field" => "Phlexi",
13
+ "phlexi" => "Phlexi",
14
+ "dom" => "DOM"
15
+ )
16
+ loader.push_dir(File.expand_path("..", __dir__))
17
+ loader.setup
18
+ end
19
+
20
+ COMPONENT_BASE = (defined?(::ApplicationComponent) ? ::ApplicationComponent : Phlex::HTML)
21
+
22
+ NIL_VALUE = :__i_phlexi_i__
23
+
24
+ class Error < StandardError; end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "phlexi/field"
@@ -0,0 +1,6 @@
1
+ module Phlexi
2
+ module Form
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,247 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: phlexi-field
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Stefan Froelich
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-09-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: phlex
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: zeitwerk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: fiber-local
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: minitest-reporters
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: standard
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: bundle-audit
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: appraisal
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: combustion
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: phlex-testing-capybara
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description: Base fields for the Phlexi libraries
182
+ email:
183
+ - sfroelich01@gmail.com
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - ".rspec"
189
+ - ".ruby-version"
190
+ - Appraisals
191
+ - CHANGELOG.md
192
+ - LICENSE.txt
193
+ - README.md
194
+ - Rakefile
195
+ - TODO
196
+ - config.ru
197
+ - gemfiles/default.gemfile
198
+ - gemfiles/default.gemfile.lock
199
+ - gemfiles/rails_7.gemfile
200
+ - gemfiles/rails_7.gemfile.lock
201
+ - lib/phlexi-field.rb
202
+ - lib/phlexi/field.rb
203
+ - lib/phlexi/field/builder.rb
204
+ - lib/phlexi/field/components/base.rb
205
+ - lib/phlexi/field/options/associations.rb
206
+ - lib/phlexi/field/options/attachments.rb
207
+ - lib/phlexi/field/options/descriptions.rb
208
+ - lib/phlexi/field/options/hints.rb
209
+ - lib/phlexi/field/options/inferred_types.rb
210
+ - lib/phlexi/field/options/labels.rb
211
+ - lib/phlexi/field/options/placeholders.rb
212
+ - lib/phlexi/field/structure/dom.rb
213
+ - lib/phlexi/field/structure/field_collection.rb
214
+ - lib/phlexi/field/structure/namespace.rb
215
+ - lib/phlexi/field/structure/namespace_collection.rb
216
+ - lib/phlexi/field/structure/node.rb
217
+ - lib/phlexi/field/theme.rb
218
+ - lib/phlexi/field/version.rb
219
+ - sig/phlexi/field.rbs
220
+ homepage: https://github.com/radioactive-labs/phlexi-field
221
+ licenses:
222
+ - MIT
223
+ metadata:
224
+ allowed_push_host: https://rubygems.org
225
+ homepage_uri: https://github.com/radioactive-labs/phlexi-field
226
+ source_code_uri: https://github.com/radioactive-labs/phlexi-field
227
+ changelog_uri: https://github.com/radioactive-labs/phlexi-field
228
+ post_install_message:
229
+ rdoc_options: []
230
+ require_paths:
231
+ - lib
232
+ required_ruby_version: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: 3.2.2
237
+ required_rubygems_version: !ruby/object:Gem::Requirement
238
+ requirements:
239
+ - - ">="
240
+ - !ruby/object:Gem::Version
241
+ version: '0'
242
+ requirements: []
243
+ rubygems_version: 3.4.10
244
+ signing_key:
245
+ specification_version: 4
246
+ summary: Base fields for the Phlexi libraries
247
+ test_files: []