phlexi-display 0.0.2 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6269ae20b09b902ba613e2f158eae3de8c5b6a900264225a0580b97fb77e6ebe
4
- data.tar.gz: 776a1c152a6e8e877f27747db06575c327ab042923b5068749631351938bae8a
3
+ metadata.gz: 3de078e9bdc0e066c1b4f3af7b9b8ef966faf1fec3779358b79fb969ff6ebdf8
4
+ data.tar.gz: 3ac581e38926c8adaf04ce1b312741d1978d7223aa83df42c18723db7be8382b
5
5
  SHA512:
6
- metadata.gz: 6e0b53271d6d515923b6c05f0fd5d83a8f54e413058b0cd023a662e3a1e1d02c62f0205ccf411cd64daf22b1ae2457676aa9d52399444421a2cabe211509a352
7
- data.tar.gz: 11bb8cd334e94ed31bcd096684cb1e7c844a13704df363dab0a540b0268440f0dcbf5a87973b7cd7ed7016e3bb45c8fe0ddbf0450a61332fd534bd0ae1f04e8d
6
+ metadata.gz: a90ec354ee5aa5a18156b6dff6e5c4b0b5220adf2a11e94004595cba305723d8398de25d6e356e0381305378800e48b188404c0c13fd42c597ead9937760eea1
7
+ data.tar.gz: 3f7a054aa04d8b3862e618370035fed1d89725fcde4ae13792122085c6301f0ea664c8c7064986ad629a7caab367617200f1677f84ef4fd473bd62393edde3d4
@@ -16,9 +16,9 @@ module Phlexi
16
16
  # @attr_reader [Symbol] key The display's key, derived from the record or explicitly set
17
17
  # @attr_reader [ActiveModel::Model, nil] object The display's associated object
18
18
  class Base < COMPONENT_BASE
19
- class Namespace < Structure::Namespace; end
19
+ class Namespace < Phlexi::Field::Structure::Namespace; end
20
20
 
21
- class FieldBuilder < Structure::FieldBuilder; end
21
+ class Builder < Builder; end
22
22
 
23
23
  attr_reader :key, :object
24
24
 
@@ -35,8 +35,8 @@ module Phlexi
35
35
  def initialize(record, attributes: {}, **options)
36
36
  @display_class = options.delete(:class)
37
37
  @attributes = attributes
38
- @namespace_klass = options.delete(:namespace_klass) || default_namespace_klass
39
- @builder_klass = options.delete(:builder_klass) || default_builder_klass
38
+ @namespace_klass = options.delete(:namespace_klass) || self.class::Namespace
39
+ @builder_klass = options.delete(:builder_klass) || self.class::Builder
40
40
  @options = options
41
41
 
42
42
  initialize_object_and_key(record)
@@ -50,10 +50,6 @@ module Phlexi
50
50
  display_template
51
51
  end
52
52
 
53
- protected
54
-
55
- attr_reader :options, :attributes, :namespace_klass, :builder_klass
56
-
57
53
  # Executes the display's content block.
58
54
  # Override this in subclasses to define a static display.
59
55
  #
@@ -62,6 +58,10 @@ module Phlexi
62
58
  instance_exec(&@_content_block) if @_content_block
63
59
  end
64
60
 
61
+ protected
62
+
63
+ attr_reader :options, :attributes, :namespace_klass, :builder_klass
64
+
65
65
  # Initializes the object and key based on the given record.
66
66
  #
67
67
  # @param record [ActiveModel::Model, Symbol, String] The display's associated record or key
@@ -75,10 +75,12 @@ module Phlexi
75
75
  @key = record
76
76
  else
77
77
  @object = record
78
- @key = if @key.nil? && object.respond_to?(:model_name) && object.model_name.respond_to?(:param_key) && object.model_name.param_key.present?
79
- object.model_name.param_key
80
- else
81
- :object
78
+ if @key.nil?
79
+ @key = if object.respond_to?(:model_name) && object.model_name.respond_to?(:param_key) && object.model_name.param_key.present?
80
+ object.model_name.param_key
81
+ else
82
+ object.class.name.demodulize.underscore
83
+ end
82
84
  end
83
85
  end
84
86
  @key = @key.to_sym
@@ -99,21 +101,10 @@ module Phlexi
99
101
  #
100
102
  # @return [Hash] The display attributes
101
103
  def display_attributes
102
- {
104
+ mix({
103
105
  id: @namespace.dom_id,
104
- class: display_class,
105
- **attributes
106
- }
107
- end
108
-
109
- private
110
-
111
- def default_namespace_klass
112
- self.class::Namespace
113
- end
114
-
115
- def default_builder_klass
116
- self.class::FieldBuilder
106
+ class: display_class
107
+ }, attributes)
117
108
  end
118
109
  end
119
110
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "phlex"
4
+
5
+ module Phlexi
6
+ module Display
7
+ # Builder class is responsible for building display fields with various options and components.
8
+ #
9
+ # @attr_reader [Structure::DOM] dom The DOM structure for the field.
10
+ # @attr_reader [Hash] options Options for the field.
11
+ # @attr_reader [Object] object The object associated with the field.
12
+ # @attr_reader [Hash] attributes Attributes for the field.
13
+ # @attr_accessor [Object] value The value of the field.
14
+ class Builder < Phlexi::Field::Builder
15
+ # Creates a label tag for the field.
16
+ #
17
+ # @param attributes [Hash] Additional attributes for the label.
18
+ # @return [Components::Label] The label component.
19
+ def label_tag(**, &)
20
+ create_component(Components::Label, :label, **, &)
21
+ end
22
+
23
+ # Creates a Placeholder tag for the field.
24
+ #
25
+ # @param attributes [Hash] Additional attributes for the placeholder.
26
+ # @return [Components::Placeholder] The placeholder component.
27
+ def placeholder_tag(**, &)
28
+ create_component(Components::Placeholder, :placeholder, **, &)
29
+ end
30
+
31
+ # Creates a Description tag for the field.
32
+ #
33
+ # @param attributes [Hash] Additional attributes for the description.
34
+ # @return [Components::Description] The description component.
35
+ def description_tag(**, &)
36
+ create_component(Components::Description, :description, **, &)
37
+ end
38
+
39
+ # Creates a string display tag for the field.
40
+ #
41
+ # @param attributes [Hash] Additional attributes for the string display.
42
+ # @return [Components::String] The string component.
43
+ def string_tag(**, &)
44
+ create_component(Components::String, :string, **, &)
45
+ end
46
+
47
+ # # Creates a text display tag for the field.
48
+ # #
49
+ # # @param attributes [Hash] Additional attributes for the text display.
50
+ # # @return [Components::Text] The text component.
51
+ # def text_tag(**, &)
52
+ # create_component(Components::Text, :text, **, &)
53
+ # end
54
+
55
+ # Creates a number display tag for the field.
56
+ #
57
+ # @param attributes [Hash] Additional attributes for the number display.
58
+ # @return [Components::Number] The number component.
59
+ def number_tag(**, &)
60
+ create_component(Components::Number, :number, **, &)
61
+ end
62
+
63
+ # Creates a datetime display for the field.
64
+ #
65
+ # @param attributes [Hash] Additional attributes for the datetime display.
66
+ # @return [Components::DateTime] The datetime component.
67
+ def datetime_tag(**, &)
68
+ create_component(Components::DateTime, :datetime, **, &)
69
+ end
70
+
71
+ # # Creates a boolean display tag for the field.
72
+ # #
73
+ # # @param attributes [Hash] Additional attributes for the boolean display.
74
+ # # @return [Components::Boolean] The boolean component.
75
+ # def boolean_tag(**, &)
76
+ # create_component(Components::Boolean, :boolean, **, &)
77
+ # end
78
+
79
+ # # Creates an association display tag for the field.
80
+ # #
81
+ # # @param attributes [Hash] Additional attributes for the association display.
82
+ # # @return [Components::Association] The association component.
83
+ # def association_tag(**, &)
84
+ # create_component(Components::Association, :association, **, &)
85
+ # end
86
+
87
+ # # Creates an attachment display tag for the field.
88
+ # #
89
+ # # @param attributes [Hash] Additional attributes for the attachment display.
90
+ # # @return [Components::Attachment] The attachment component.
91
+ # def attachment_tag(**, &)
92
+ # create_component(Components::Attachment, :attachment, **, &)
93
+ # end
94
+
95
+ # Wraps the field with additional markup.
96
+ #
97
+ # @param attributes [Hash] Additional attributes for the wrapper.
98
+ # @yield [block] The block to be executed within the wrapper.
99
+ # @return [Components::Wrapper] The wrapper component.
100
+ def wrapped(**, &)
101
+ create_component(Components::Wrapper, :wrapper, **, &)
102
+ end
103
+
104
+ protected
105
+
106
+ def create_component(component_class, theme_key, **attributes, &)
107
+ theme_key = attributes.delete(:theme) || theme_key
108
+ attributes = mix({class: themed(theme_key)}, attributes) unless attributes.key?(:class!)
109
+ component_class.new(self, **attributes, &)
110
+ end
111
+
112
+ def themed(component)
113
+ Phlexi::Display::Theme.instance.resolve_theme(component)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -3,35 +3,7 @@
3
3
  module Phlexi
4
4
  module Display
5
5
  module Components
6
- class Base < COMPONENT_BASE
7
- attr_reader :field, :attributes
8
-
9
- def initialize(field, **attributes)
10
- @field = field
11
- @attributes = attributes
12
-
13
- build_attributes
14
- append_attribute_classes
15
- end
16
-
17
- protected
18
-
19
- def build_attributes
20
- attributes.fetch(:id) { attributes[:id] = "#{field.dom.id}_#{component_name}" }
21
- end
22
-
23
- def append_attribute_classes
24
- return if attributes[:class] == false
25
-
26
- attributes[:class] = tokens(
27
- component_name,
28
- attributes[:class]
29
- )
30
- end
31
-
32
- def component_name
33
- @component_name ||= self.class.name.demodulize.underscore
34
- end
6
+ class Base < Phlexi::Field::Components::Base
35
7
  end
36
8
  end
37
9
  end
@@ -0,0 +1,22 @@
1
+ module Phlexi
2
+ module Display
3
+ class Theme
4
+ include Phlexi::Field::Theme
5
+
6
+ DEFAULT_THEME = {
7
+ label: nil,
8
+ description: nil,
9
+ placeholder: nil,
10
+ string: nil,
11
+ number: :string,
12
+ datetime: :string,
13
+ attachment: :string,
14
+ wrapper: nil
15
+ }.freeze
16
+
17
+ def theme
18
+ DEFAULT_THEME
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlexi
4
4
  module Display
5
- VERSION = "0.0.2"
5
+ VERSION = "0.0.3"
6
6
  end
7
7
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "zeitwerk"
4
4
  require "phlex"
5
+ require "phlexi-field"
5
6
  require "active_support/core_ext/object/blank"
6
7
 
7
8
  module Phlexi
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlexi-display
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-26 00:00:00.000000000 Z
11
+ date: 2024-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: phlex
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: phlexi-field
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'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rake
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -185,6 +199,7 @@ files:
185
199
  - lib/phlexi-display.rb
186
200
  - lib/phlexi/display.rb
187
201
  - lib/phlexi/display/base.rb
202
+ - lib/phlexi/display/builder.rb
188
203
  - lib/phlexi/display/components/base.rb
189
204
  - lib/phlexi/display/components/concerns/displays_value.rb
190
205
  - lib/phlexi/display/components/date_time.rb
@@ -195,21 +210,7 @@ files:
195
210
  - lib/phlexi/display/components/placeholder.rb
196
211
  - lib/phlexi/display/components/string.rb
197
212
  - lib/phlexi/display/components/wrapper.rb
198
- - lib/phlexi/display/field_options/associations.rb
199
- - lib/phlexi/display/field_options/attachments.rb
200
- - lib/phlexi/display/field_options/description.rb
201
- - lib/phlexi/display/field_options/hints.rb
202
- - lib/phlexi/display/field_options/inferred_types.rb
203
- - lib/phlexi/display/field_options/labels.rb
204
- - lib/phlexi/display/field_options/placeholders.rb
205
- - lib/phlexi/display/field_options/themes.rb
206
- - lib/phlexi/display/option_mapper.rb
207
- - lib/phlexi/display/structure/dom.rb
208
- - lib/phlexi/display/structure/field_builder.rb
209
- - lib/phlexi/display/structure/field_collection.rb
210
- - lib/phlexi/display/structure/namespace.rb
211
- - lib/phlexi/display/structure/namespace_collection.rb
212
- - lib/phlexi/display/structure/node.rb
213
+ - lib/phlexi/display/theme.rb
213
214
  - lib/phlexi/display/version.rb
214
215
  - sig/phlexi/display.rbs
215
216
  homepage: https://github.com/radioactive-labs/phlexi-display
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- module FieldOptions
6
- module Associations
7
- protected
8
-
9
- def association_reflection
10
- @association_reflection ||= find_association_reflection
11
- end
12
-
13
- def find_association_reflection
14
- if object.class.respond_to?(:reflect_on_association)
15
- object.class.reflect_on_association(key)
16
- end
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- module FieldOptions
6
- module Attachments
7
- protected
8
-
9
- def attachment_reflection
10
- @attachment_reflection ||= find_attachment_reflection
11
- end
12
-
13
- def find_attachment_reflection
14
- if object.class.respond_to?(:reflect_on_attachment)
15
- object.class.reflect_on_attachment(key)
16
- end
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- module FieldOptions
6
- module Description
7
- def description(description = nil)
8
- if description.nil?
9
- options[:description]
10
- else
11
- options[:description] = description
12
- self
13
- end
14
- end
15
-
16
- def has_description?
17
- description.present?
18
- end
19
- end
20
- end
21
- end
22
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- module FieldOptions
6
- module Hints
7
- def hint(hint = nil)
8
- if hint.nil?
9
- options[:hint]
10
- else
11
- options[:hint] = hint
12
- self
13
- end
14
- end
15
-
16
- def has_hint?
17
- hint.present?
18
- end
19
- end
20
- end
21
- end
22
- end
@@ -1,129 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bigdecimal"
4
-
5
- module Phlexi
6
- module Display
7
- module FieldOptions
8
- module InferredTypes
9
- def inferred_db_type
10
- @inferred_db_type ||= infer_db_type
11
- end
12
-
13
- def inferred_display_component
14
- @inferred_display_component ||= infer_display_component
15
- end
16
-
17
- private
18
-
19
- def infer_display_component
20
- case inferred_db_type
21
- when :string, :text
22
- infer_string_display_type(key)
23
- when :integer, :float, :decimal
24
- :number
25
- when :date, :datetime, :time
26
- :date
27
- when :boolean
28
- :boolean
29
- when :json, :jsonb, :hstore
30
- :code
31
- else
32
- if association_reflection
33
- :association
34
- elsif attachment_reflection
35
- :attachment
36
- else
37
- :text
38
- end
39
- end
40
- end
41
-
42
- def infer_db_type
43
- if object.class.respond_to?(:columns_hash)
44
- # ActiveRecord object
45
- column = object.class.columns_hash[key.to_s]
46
- return column.type if column
47
- end
48
-
49
- if object.class.respond_to?(:attribute_types)
50
- # ActiveModel::Attributes
51
- custom_type = object.class.attribute_types[key.to_s]
52
- return custom_type.type if custom_type
53
- end
54
-
55
- # Check if object responds to the key
56
- if object.respond_to?(key)
57
- # Fallback to inferring type from the value
58
- return infer_db_type_from_value(object.send(key))
59
- end
60
-
61
- # Default to string if we can't determine the type
62
- :string
63
- end
64
-
65
- def infer_db_type_from_value(value)
66
- case value
67
- when Integer
68
- :integer
69
- when Float
70
- :float
71
- when BigDecimal
72
- :decimal
73
- when TrueClass, FalseClass
74
- :boolean
75
- when Date
76
- :date
77
- when Time, DateTime
78
- :datetime
79
- when Hash
80
- :json
81
- else
82
- :string
83
- end
84
- end
85
-
86
- def infer_string_display_type(key)
87
- key = key.to_s.downcase
88
-
89
- return :password if is_password_field?
90
-
91
- custom_type = custom_string_display_type(key)
92
- return custom_type if custom_type
93
-
94
- :text
95
- end
96
-
97
- def custom_string_display_type(key)
98
- custom_mappings = {
99
- /url$|^link|^site/ => :url,
100
- /^email/ => :email,
101
- /phone|tel(ephone)?/ => :phone,
102
- /^time/ => :time,
103
- /^date/ => :date,
104
- /^number|_count$|_amount$/ => :number,
105
- /^color/ => :color
106
- }
107
-
108
- custom_mappings.each do |pattern, type|
109
- return type if key.match?(pattern)
110
- end
111
-
112
- nil
113
- end
114
-
115
- def is_password_field?
116
- key = self.key.to_s.downcase
117
-
118
- exact_matches = ["password"]
119
- prefixes = ["encrypted_"]
120
- suffixes = ["_password", "_digest", "_hash"]
121
-
122
- exact_matches.include?(key) ||
123
- prefixes.any? { |prefix| key.start_with?(prefix) } ||
124
- suffixes.any? { |suffix| key.end_with?(suffix) }
125
- end
126
- end
127
- end
128
- end
129
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- module FieldOptions
6
- module Labels
7
- def label(label = nil)
8
- if label.nil?
9
- options[:label] = options.fetch(:label) { calculate_label }
10
- else
11
- options[:label] = label
12
- self
13
- end
14
- end
15
-
16
- private
17
-
18
- def calculate_label
19
- if object.class.respond_to?(:human_attribute_name)
20
- object.class.human_attribute_name(key.to_s, {base: object})
21
- else
22
- key.to_s.humanize
23
- end
24
- end
25
- end
26
- end
27
- end
28
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- module FieldOptions
6
- module Placeholders
7
- def placeholder(placeholder = nil)
8
- if placeholder.nil?
9
- options[:placeholder] || "-"
10
- else
11
- options[:placeholder] = placeholder
12
- self
13
- end
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,132 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- module FieldOptions
6
- module Themes
7
- # Resolves theme classes for components based on their type.
8
- #
9
- # This method is responsible for determining the appropriate CSS classes for a given display component.
10
- # It supports a hierarchical theming system, allowing for cascading themes and easy customization.
11
- #
12
- # @param component [Symbol, String] The type of display component (e.g., :text, :date, :boolean)
13
- #
14
- # @return [String, nil] A string of CSS classes for the component, or nil if no theme is applied
15
- #
16
- # @example Basic usage
17
- # themed(:text)
18
- # # => "text-gray-700 text-sm"
19
- #
20
- # @example Cascading themes
21
- # # Assuming email inherits from text in the theme definition
22
- # themed(:email)
23
- # # => "text-gray-700 text-sm text-blue-600 underline"
24
- #
25
- # @note The actual CSS classes returned will depend on the theme definitions in the `theme` hash
26
- # and any overrides specified in the `options` hash.
27
- #
28
- # @see #resolve_theme
29
- # @see #theme
30
- def themed(component)
31
- return unless component
32
-
33
- resolve_theme(component)
34
- end
35
-
36
- protected
37
-
38
- # Recursively resolves the theme for a given property, handling nested symbol references
39
- #
40
- # @param property [Symbol, String] The theme property to resolve
41
- # @param visited [Set] Set of already visited properties to prevent infinite recursion
42
- # @return [String, nil] The resolved theme value or nil if not found
43
- #
44
- # @example Resolving a nested theme
45
- # # Assuming the theme is: { email: :text, text: "text-gray-700" }
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
-
60
- # Retrieves or initializes the theme hash for the display builder.
61
- #
62
- # This method returns a hash containing theme definitions for various display components.
63
- # If a theme has been explicitly set in the options, it returns that. Otherwise, it
64
- # initializes and returns a default theme.
65
- #
66
- # The theme hash defines CSS classes or references to other theme keys for different
67
- # components.
68
- #
69
- # @return [Hash] A hash containing theme definitions for display components
70
- #
71
- # @example Accessing the theme
72
- # theme[:text]
73
- # # => "text-gray-700 text-sm"
74
- #
75
- # @example Theme inheritance
76
- # theme[:email] # Returns :text, indicating email inherits text's theme
77
- #
78
- # @note The actual content of the theme hash depends on the default_theme method
79
- # and any theme overrides specified in the options when initializing the field builder.
80
- #
81
- # @see #default_theme
82
- def theme
83
- @theme ||= options[:theme] || default_theme
84
- end
85
-
86
- # Defines and returns the default theme hash for the display builder.
87
- #
88
- # This method returns a hash containing the base theme definitions for various components.
89
- # It sets up the default styling and relationships between different components.
90
- # The theme uses a combination of explicit CSS classes and symbolic references to other theme keys,
91
- # allowing for a flexible and inheritance-based theming system.
92
- #
93
- # @return [Hash] A frozen hash containing default theme definitions for components
94
- #
95
- # @example Accessing the default theme
96
- # default_theme[:text]
97
- # # => "text-gray-700 text-sm"
98
- #
99
- # @example Theme inheritance
100
- # default_theme[:email]
101
- # # => :text (indicates that :email inherits from :text)
102
- #
103
- # @note This method returns a frozen hash to prevent accidental modifications.
104
- # To customize the theme, users should provide their own theme hash when initializing the display builder.
105
- #
106
- # @see #theme
107
- def default_theme
108
- {
109
- label: "text-base font-bold text-gray-500 dark:text-gray-400 mb-1",
110
- description: "text-sm text-gray-400 dark:text-gray-500",
111
- placeholder: "text-xl font-semibold text-gray-500 dark:text-gray-300 mb-1 italic",
112
- string: "text-xl font-semibold text-gray-900 dark:text-white mb-1",
113
- # text: :string,
114
- number: :string,
115
- datetime: :string,
116
- # boolean: :string,
117
- # code: :string,
118
- # email: :text,
119
- # url: :text,
120
- # phone: :text,
121
- # color: :text,
122
- # search: :text,
123
- # password: :string,
124
- # association: :string,
125
- attachment: :string,
126
- wrapper: nil
127
- }.freeze
128
- end
129
- end
130
- end
131
- end
132
- end
@@ -1,154 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- # OptionMapper is responsible for converting a collection of objects into a hash of options
6
- # suitable for form controls, such as `select > options`.
7
- # Both values and labels are converted to strings.
8
- #
9
- # @example Basic usage
10
- # collection = [["First", 1], ["Second", 2]]
11
- # mapper = OptionMapper.new(collection)
12
- # mapper.each { |value, label| puts "#{value}: #{label}" }
13
- #
14
- # @example Using with ActiveRecord objects
15
- # users = User.all
16
- # mapper = OptionMapper.new(users)
17
- # mapper.each { |id, name| puts "#{id}: #{name}" }
18
- #
19
- # @example Array access with different value types
20
- # mapper = OptionMapper.new([["Integer", 1], ["String", "2"], ["Symbol", :three]])
21
- # puts mapper["1"] # Output: "Integer"
22
- # puts mapper["2"] # Output: "String"
23
- # puts mapper["three"] # Output: "Symbol"
24
- #
25
- # @note This class is thread-safe as it doesn't maintain mutable state.
26
- class OptionMapper
27
- include Enumerable
28
-
29
- # Initializes a new OptionMapper instance.
30
- #
31
- # @param collection [#call, #to_a] The collection to be mapped.
32
- # @param label_method [Symbol, nil] The method to call on each object to get the label.
33
- # @param value_method [Symbol, nil] The method to call on each object to get the value.
34
- def initialize(collection, label_method: nil, value_method: nil)
35
- @raw_collection = collection
36
- @label_method = label_method
37
- @value_method = value_method
38
- end
39
-
40
- # Iterates over the collection, yielding value-label pairs.
41
- #
42
- # @yieldparam value [String] The string value for the current item.
43
- # @yieldparam label [String] The string label for the current item.
44
- # @return [Enumerator] If no block is given.
45
- def each(&)
46
- collection.each(&)
47
- end
48
-
49
- # @return [Array<String>] An array of all labels in the collection.
50
- def labels
51
- collection.values
52
- end
53
-
54
- # @return [Array<String>] An array of all values in the collection.
55
- def values
56
- collection.keys
57
- end
58
-
59
- # Retrieves the label for a given value.
60
- #
61
- # @param value [#to_s] The value to look up.
62
- # @return [String, nil] The label corresponding to the value, or nil if not found.
63
- def [](value)
64
- collection[value.to_s]
65
- end
66
-
67
- private
68
-
69
- # @return [Hash<String, String>] The materialized collection as a hash of string value => string label.
70
- def collection
71
- @collection ||= materialize_collection(@raw_collection)
72
- end
73
-
74
- # Converts the raw collection into a materialized hash.
75
- #
76
- # @param collection [#call, #to_a] The collection to be materialized.
77
- # @return [Hash<String, String>] The materialized collection as a hash of string value => string label.
78
- # @raise [ArgumentError] If the collection cannot be materialized into an enumerable.
79
- def materialize_collection(collection)
80
- case collection
81
- in Hash => hash
82
- hash.transform_keys(&:to_s).transform_values(&:to_s)
83
- in Array => arr
84
- array_to_hash(arr)
85
- in Range => range
86
- range_to_hash(range)
87
- in Proc => proc
88
- materialize_collection(proc.call)
89
- in Symbol
90
- raise ArgumentError, "Symbol collections are not supported in this context"
91
- in Set => set
92
- array_to_hash(set.to_a)
93
- else
94
- array_to_hash(Array(collection))
95
- end
96
- rescue ArgumentError
97
- # Rails.logger.warn("Unhandled inclusion collection type: #{e}")
98
- {}
99
- end
100
-
101
- # Converts an array to a hash using detected or specified methods.
102
- #
103
- # @param array [Array] The array to convert.
104
- # @return [Hash<String, String>] The resulting hash of string value => string label.
105
- def array_to_hash(array)
106
- sample = array.first || array.last
107
- methods = detect_methods_for_sample(sample)
108
-
109
- array.each_with_object({}) do |item, hash|
110
- value = item.public_send(methods[:value]).to_s
111
- label = item.public_send(methods[:label]).to_s
112
- hash[value] = label
113
- end
114
- end
115
-
116
- # Converts a range to a hash.
117
- #
118
- # @param range [Range] The range to convert.
119
- # @return [Hash<String, String>] The range converted to a hash of string value => string label.
120
- # @raise [ArgumentError] If the range is unbounded.
121
- def range_to_hash(range)
122
- raise ArgumentError, "Cannot safely materialize an unbounded range" if range.begin.nil? || range.end.nil?
123
-
124
- range.each_with_object({}) { |value, hash| hash[value.to_s] = value.to_s }
125
- end
126
-
127
- # Detects suitable methods for label and value from a sample object.
128
- #
129
- # @param sample [Object] A sample object from the collection.
130
- # @return [Hash{Symbol => Symbol}] A hash containing :label and :value keys with corresponding method names.
131
- def detect_methods_for_sample(sample)
132
- case sample
133
- when Array
134
- {value: :last, label: :first}
135
- else
136
- {
137
- value: @value_method || collection_value_methods.find { |m| sample.respond_to?(m) },
138
- label: @label_method || collection_label_methods.find { |m| sample.respond_to?(m) }
139
- }
140
- end
141
- end
142
-
143
- # @return [Array<Symbol>] An array of method names to try for collection values.
144
- def collection_value_methods
145
- @collection_value_methods ||= %i[id to_s].freeze
146
- end
147
-
148
- # @return [Array<Symbol>] An array of method names to try for collection labels.
149
- def collection_label_methods
150
- @collection_label_methods ||= %i[to_label name title to_s].freeze
151
- end
152
- end
153
- end
154
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- module Structure
6
- # Generates DOM IDs for a Field, Namespace, or Node based on
7
- # norms that were established by Rails. These can be used outside of 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
- # One-liner way of walking from the current node all the way up to the parent.
31
- def lineage
32
- @lineage ||= Enumerator.produce(@field, &:parent).take_while(&:itself).reverse
33
- end
34
-
35
- # Emit the id and value in an HTML tag-ish that doesn't have an element.
36
- def inspect
37
- "<#{self.class.name} id=#{id.inspect} value=#{value.inspect}/>"
38
- end
39
- end
40
- end
41
- end
42
- end
@@ -1,160 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "phlex"
4
-
5
- module Phlexi
6
- module Display
7
- module Structure
8
- # FieldBuilder class is responsible for building display fields with various options and components.
9
- #
10
- # @attr_reader [Structure::DOM] dom The DOM structure for the field.
11
- # @attr_reader [Hash] options Options for the field.
12
- # @attr_reader [Object] object The object associated with the field.
13
- # @attr_reader [Hash] attributes Attributes for the field.
14
- # @attr_accessor [Object] value The value of the field.
15
- class FieldBuilder < Node
16
- include Phlex::Helpers
17
- include FieldOptions::Themes
18
- include FieldOptions::Associations
19
- include FieldOptions::Attachments
20
- include FieldOptions::InferredTypes
21
- include FieldOptions::Labels
22
- include FieldOptions::Placeholders
23
- include FieldOptions::Description
24
- # include FieldOptions::Hints
25
-
26
- attr_reader :dom, :options, :object, :value
27
-
28
- # Initializes a new FieldBuilder instance.
29
- #
30
- # @param key [Symbol, String] The key for the field.
31
- # @param parent [Structure::Namespace] The parent object.
32
- # @param object [Object, nil] The associated object.
33
- # @param value [Object] The initial value for the field.
34
- # @param options [Hash] Additional options for the field.
35
- def initialize(key, parent:, object: nil, value: NIL_VALUE, **options)
36
- super(key, parent: parent)
37
-
38
- @object = object
39
- @value = determine_value(value)
40
- @options = options
41
- @dom = Structure::DOM.new(field: self)
42
- end
43
-
44
- # Creates a label tag for the field.
45
- #
46
- # @param attributes [Hash] Additional attributes for the label.
47
- # @return [Components::Label] The label component.
48
- def label_tag(**attributes, &)
49
- create_component(Components::Label, :label, **attributes, &)
50
- end
51
-
52
- # Creates a Placeholder tag for the field.
53
- #
54
- # @param attributes [Hash] Additional attributes for the placeholder.
55
- # @return [Components::Placeholder] The placeholder component.
56
- def placeholder_tag(**attributes, &)
57
- create_component(Components::Placeholder, :placeholder, **attributes, &)
58
- end
59
-
60
- # Creates a Description tag for the field.
61
- #
62
- # @param attributes [Hash] Additional attributes for the description.
63
- # @return [Components::Description] The description component.
64
- def description_tag(**attributes, &)
65
- create_component(Components::Description, :description, **attributes, &)
66
- end
67
-
68
- # Creates a string display tag for the field.
69
- #
70
- # @param attributes [Hash] Additional attributes for the string display.
71
- # @return [Components::String] The string component.
72
- def string_tag(**attributes, &)
73
- create_component(Components::String, :string, **attributes, &)
74
- end
75
-
76
- # # Creates a text display tag for the field.
77
- # #
78
- # # @param attributes [Hash] Additional attributes for the text display.
79
- # # @return [Components::Text] The text component.
80
- # def text_tag(**attributes, &)
81
- # create_component(Components::Text, :text, **attributes, &)
82
- # end
83
-
84
- # Creates a number display tag for the field.
85
- #
86
- # @param attributes [Hash] Additional attributes for the number display.
87
- # @return [Components::Number] The number component.
88
- def number_tag(**attributes, &)
89
- create_component(Components::Number, :number, **attributes, &)
90
- end
91
-
92
- # Creates a datetime display for the field.
93
- #
94
- # @param attributes [Hash] Additional attributes for the datetime display.
95
- # @return [Components::DateTime] The datetime component.
96
- def datetime_tag(**attributes, &)
97
- create_component(Components::DateTime, :datetime, **attributes, &)
98
- end
99
-
100
- # # Creates a boolean display tag for the field.
101
- # #
102
- # # @param attributes [Hash] Additional attributes for the boolean display.
103
- # # @return [Components::Boolean] The boolean component.
104
- # def boolean_tag(**attributes, &)
105
- # create_component(Components::Boolean, :boolean, **attributes, &)
106
- # end
107
-
108
- # # Creates an association display tag for the field.
109
- # #
110
- # # @param attributes [Hash] Additional attributes for the association display.
111
- # # @return [Components::Association] The association component.
112
- # def association_tag(**attributes, &)
113
- # create_component(Components::Association, :association, **attributes, &)
114
- # end
115
-
116
- # # Creates an attachment display tag for the field.
117
- # #
118
- # # @param attributes [Hash] Additional attributes for the attachment display.
119
- # # @return [Components::Attachment] The attachment component.
120
- # def attachment_tag(**attributes, &)
121
- # create_component(Components::Attachment, :attachment, **attributes, &)
122
- # end
123
-
124
- # Wraps the field with additional markup.
125
- #
126
- # @param attributes [Hash] Additional attributes for the wrapper.
127
- # @yield [block] The block to be executed within the wrapper.
128
- # @return [Components::Wrapper] The wrapper component.
129
- def wrapped(**attributes, &)
130
- create_component(Components::Wrapper, :wrapper, **attributes, &)
131
- end
132
-
133
- # Creates a repeated field collection.
134
- #
135
- # @param range [#each] The collection of items to generate displays for.
136
- # @yield [block] The block to be executed for each item in the collection.
137
- # @return [FieldCollection] The field collection.
138
- def repeated(collection = [], &)
139
- FieldCollection.new(field: self, collection:, &)
140
- end
141
-
142
- protected
143
-
144
- def create_component(component_class, theme_key, **attributes, &)
145
- component_class.new(self, class: component_class_for(theme_key, attributes), **attributes, &)
146
- end
147
-
148
- def component_class_for(theme_key, attributes)
149
- attributes.delete(:class) || themed(attributes.key?(:theme) ? attributes.delete(:theme) : theme_key)
150
- end
151
-
152
- def determine_value(value)
153
- return value unless value == NIL_VALUE
154
-
155
- object.respond_to?(key) ? object.public_send(key) : nil
156
- end
157
- end
158
- end
159
- end
160
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
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 = collection
28
- each(&) if block_given?
29
- end
30
-
31
- def each(&)
32
- @collection.each.with_index do |item, index|
33
- yield Builder.new(item, @field, index)
34
- end
35
- end
36
- end
37
- end
38
- end
39
- end
@@ -1,123 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
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
- # display like this:
43
- #
44
- # ```ruby
45
- # Phlexi::Display(user) do |display|
46
- # display.nest_one :profile do |profile|
47
- # render profile.field(:gender).text
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::Display(user) do |display|
61
- # render display.field(:email).text
62
- # render display.field(:name).text
63
- # display.nest_many :addresses do |address|
64
- # render address.field(:street).text
65
- # render address.field(:state).text
66
- # render address.field(:zip).text
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
- @object.send(key) if @object.respond_to? key
111
- end
112
-
113
- private
114
-
115
- # Checks if the child exists. If it does then it returns that. If it doesn't, it will
116
- # build the child.
117
- def create_child(key, child_class, **kwargs, &block)
118
- @children.fetch(key) { @children[key] = child_class.new(key, parent: self, **kwargs, &block) }
119
- end
120
- end
121
- end
122
- end
123
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
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
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phlexi
4
- module Display
5
- module Structure
6
- # Superclass for Namespace and Field classes. Represents a node in the display 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
- end
22
- end
23
- end
24
- end