phlexi-display 0.0.2 → 0.0.3

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