phlexi-field 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []