phlexi-display 0.0.1 → 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 +4 -4
- data/lib/phlexi/display/base.rb +37 -169
- data/lib/phlexi/display/builder.rb +117 -0
- data/lib/phlexi/display/components/base.rb +1 -42
- data/lib/phlexi/display/components/concerns/displays_value.rb +54 -0
- data/lib/phlexi/display/components/date_time.rb +49 -0
- data/lib/phlexi/display/components/{error.rb → description.rb} +5 -5
- data/lib/phlexi/display/components/hint.rb +1 -1
- data/lib/phlexi/display/components/label.rb +3 -15
- data/lib/phlexi/display/components/number.rb +37 -0
- data/lib/phlexi/display/components/placeholder.rb +15 -0
- data/lib/phlexi/display/components/string.rb +17 -0
- data/lib/phlexi/display/components/wrapper.rb +4 -18
- data/lib/phlexi/display/theme.rb +22 -0
- data/lib/phlexi/display/version.rb +1 -1
- data/lib/phlexi/display.rb +1 -1
- metadata +24 -43
- data/lib/phlexi/display/components/checkbox.rb +0 -48
- data/lib/phlexi/display/components/collection_checkboxes.rb +0 -44
- data/lib/phlexi/display/components/collection_radio_buttons.rb +0 -35
- data/lib/phlexi/display/components/concerns/handles_array_input.rb +0 -21
- data/lib/phlexi/display/components/concerns/handles_input.rb +0 -53
- data/lib/phlexi/display/components/concerns/has_options.rb +0 -37
- data/lib/phlexi/display/components/concerns/submits_form.rb +0 -39
- data/lib/phlexi/display/components/file_input.rb +0 -32
- data/lib/phlexi/display/components/full_error.rb +0 -21
- data/lib/phlexi/display/components/input.rb +0 -84
- data/lib/phlexi/display/components/input_array.rb +0 -45
- data/lib/phlexi/display/components/radio_button.rb +0 -41
- data/lib/phlexi/display/components/select.rb +0 -69
- data/lib/phlexi/display/components/submit_button.rb +0 -41
- data/lib/phlexi/display/components/textarea.rb +0 -34
- data/lib/phlexi/display/field_options/associations.rb +0 -21
- data/lib/phlexi/display/field_options/autofocus.rb +0 -18
- data/lib/phlexi/display/field_options/collection.rb +0 -54
- data/lib/phlexi/display/field_options/disabled.rb +0 -18
- data/lib/phlexi/display/field_options/errors.rb +0 -92
- data/lib/phlexi/display/field_options/hints.rb +0 -22
- data/lib/phlexi/display/field_options/inferred_types.rb +0 -155
- data/lib/phlexi/display/field_options/labels.rb +0 -28
- data/lib/phlexi/display/field_options/length.rb +0 -53
- data/lib/phlexi/display/field_options/limit.rb +0 -66
- data/lib/phlexi/display/field_options/min_max.rb +0 -92
- data/lib/phlexi/display/field_options/multiple.rb +0 -65
- data/lib/phlexi/display/field_options/pattern.rb +0 -38
- data/lib/phlexi/display/field_options/placeholder.rb +0 -18
- data/lib/phlexi/display/field_options/readonly.rb +0 -18
- data/lib/phlexi/display/field_options/required.rb +0 -37
- data/lib/phlexi/display/field_options/themes.rb +0 -207
- data/lib/phlexi/display/field_options/validators.rb +0 -48
- data/lib/phlexi/display/option_mapper.rb +0 -154
- data/lib/phlexi/display/structure/dom.rb +0 -62
- data/lib/phlexi/display/structure/field_builder.rb +0 -236
- data/lib/phlexi/display/structure/field_collection.rb +0 -54
- data/lib/phlexi/display/structure/namespace.rb +0 -135
- data/lib/phlexi/display/structure/namespace_collection.rb +0 -48
- data/lib/phlexi/display/structure/node.rb +0 -18
@@ -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,62 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Phlexi
|
4
|
-
module Display
|
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 outsidef or Rails in
|
8
|
-
# other Ruby web frameworks since it has now 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 the web form.
|
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 seperates
|
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
|
@@ -1,236 +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::Associations
|
18
|
-
include FieldOptions::Themes
|
19
|
-
include FieldOptions::Validators
|
20
|
-
include FieldOptions::Labels
|
21
|
-
include FieldOptions::Hints
|
22
|
-
include FieldOptions::Errors
|
23
|
-
include FieldOptions::InferredTypes
|
24
|
-
include FieldOptions::Collection
|
25
|
-
include FieldOptions::Placeholder
|
26
|
-
include FieldOptions::Required
|
27
|
-
include FieldOptions::Autofocus
|
28
|
-
include FieldOptions::Disabled
|
29
|
-
include FieldOptions::Readonly
|
30
|
-
include FieldOptions::Length
|
31
|
-
include FieldOptions::MinMax
|
32
|
-
include FieldOptions::Pattern
|
33
|
-
include FieldOptions::Multiple
|
34
|
-
include FieldOptions::Limit
|
35
|
-
|
36
|
-
attr_reader :dom, :options, :object, :input_attributes, :value
|
37
|
-
|
38
|
-
# Initializes a new FieldBuilder instance.
|
39
|
-
#
|
40
|
-
# @param key [Symbol, String] The key for the field.
|
41
|
-
# @param parent [Structure::Namespace] The parent object.
|
42
|
-
# @param object [Object, nil] The associated object.
|
43
|
-
# @param value [Object] The initial value for the field.
|
44
|
-
# @param input_attributes [Hash] Default attributes to apply to input fields.
|
45
|
-
# @param options [Hash] Additional options for the field.
|
46
|
-
def initialize(key, parent:, object: nil, value: NIL_VALUE, input_attributes: {}, **options)
|
47
|
-
super(key, parent: parent)
|
48
|
-
|
49
|
-
@object = object
|
50
|
-
@value = determine_initial_value(value)
|
51
|
-
@input_attributes = input_attributes
|
52
|
-
@options = options
|
53
|
-
@dom = Structure::DOM.new(field: self)
|
54
|
-
end
|
55
|
-
|
56
|
-
# Creates a label tag for the field.
|
57
|
-
#
|
58
|
-
# @param attributes [Hash] Additional attributes for the label.
|
59
|
-
# @return [Components::Label] The label component.
|
60
|
-
def label_tag(**attributes, &)
|
61
|
-
create_component(Components::Label, :label, **attributes, &)
|
62
|
-
end
|
63
|
-
|
64
|
-
# Creates an input tag for the field.
|
65
|
-
#
|
66
|
-
# @param attributes [Hash] Additional attributes for the input.
|
67
|
-
# @return [Components::Input] The input component.
|
68
|
-
def input_tag(**attributes, &)
|
69
|
-
create_component(Components::Input, :input, **attributes, &)
|
70
|
-
end
|
71
|
-
|
72
|
-
def file_input_tag(**attributes, &)
|
73
|
-
create_component(Components::FileInput, :file, **attributes, &)
|
74
|
-
end
|
75
|
-
|
76
|
-
# Creates a checkbox tag for the field.
|
77
|
-
#
|
78
|
-
# @param attributes [Hash] Additional attributes for the checkbox.
|
79
|
-
# @return [Components::Checkbox] The checkbox component.
|
80
|
-
def checkbox_tag(**attributes, &)
|
81
|
-
create_component(Components::Checkbox, :checkbox, **attributes, &)
|
82
|
-
end
|
83
|
-
|
84
|
-
# Creates collection checkboxes for the field.
|
85
|
-
#
|
86
|
-
# @param attributes [Hash] Additional attributes for the collection checkboxes.
|
87
|
-
# @yield [block] The block to be executed for each checkbox.
|
88
|
-
# @return [Components::CollectionCheckboxes] The collection checkboxes component.
|
89
|
-
def collection_checkboxes_tag(**attributes, &)
|
90
|
-
create_component(Components::CollectionCheckboxes, :collection_checkboxes, **attributes, &)
|
91
|
-
end
|
92
|
-
|
93
|
-
# Creates a radio button tag for the field.
|
94
|
-
#
|
95
|
-
# @param attributes [Hash] Additional attributes for the radio button.
|
96
|
-
# @return [Components::RadioButton] The radio button component.
|
97
|
-
def radio_button_tag(**attributes, &)
|
98
|
-
create_component(Components::RadioButton, :radio, **attributes, &)
|
99
|
-
end
|
100
|
-
|
101
|
-
# Creates collection radio buttons for the field.
|
102
|
-
#
|
103
|
-
# @param attributes [Hash] Additional attributes for the collection radio buttons.
|
104
|
-
# @yield [block] The block to be executed for each radio button.
|
105
|
-
# @return [Components::CollectionRadioButtons] The collection radio buttons component.
|
106
|
-
def collection_radio_buttons_tag(**attributes, &)
|
107
|
-
create_component(Components::CollectionRadioButtons, :collection_radio_buttons, **attributes, &)
|
108
|
-
end
|
109
|
-
|
110
|
-
# Creates a textarea tag for the field.
|
111
|
-
#
|
112
|
-
# @param attributes [Hash] Additional attributes for the textarea.
|
113
|
-
# @return [Components::Textarea] The textarea component.
|
114
|
-
def textarea_tag(**attributes, &)
|
115
|
-
create_component(Components::Textarea, :textarea, **attributes, &)
|
116
|
-
end
|
117
|
-
|
118
|
-
# Creates a select tag for the field.
|
119
|
-
#
|
120
|
-
# @param attributes [Hash] Additional attributes for the select.
|
121
|
-
# @return [Components::Select] The select component.
|
122
|
-
def select_tag(**attributes, &)
|
123
|
-
create_component(Phlex::UI::Select, :select, **attributes, &)
|
124
|
-
end
|
125
|
-
|
126
|
-
def input_array_tag(**attributes, &)
|
127
|
-
create_component(Components::InputArray, :array, **attributes, &)
|
128
|
-
end
|
129
|
-
|
130
|
-
# Creates a hint tag for the field.
|
131
|
-
#
|
132
|
-
# @param attributes [Hash] Additional attributes for the hint.
|
133
|
-
# @return [Components::Hint] The hint component.
|
134
|
-
def hint_tag(**attributes, &)
|
135
|
-
create_component(Components::Hint, :hint, **attributes, &)
|
136
|
-
end
|
137
|
-
|
138
|
-
# Creates an error tag for the field.
|
139
|
-
#
|
140
|
-
# @param attributes [Hash] Additional attributes for the error.
|
141
|
-
# @return [Components::Error] The error component.
|
142
|
-
def error_tag(**attributes, &)
|
143
|
-
create_component(Components::Error, :error, **attributes, &)
|
144
|
-
end
|
145
|
-
|
146
|
-
# Creates a full error tag for the field.
|
147
|
-
#
|
148
|
-
# @param attributes [Hash] Additional attributes for the full error.
|
149
|
-
# @return [Components::FullError] The full error component.
|
150
|
-
def full_error_tag(**attributes, &)
|
151
|
-
create_component(Components::FullError, :full_error, **attributes, &)
|
152
|
-
end
|
153
|
-
|
154
|
-
# Wraps the field with additional markup.
|
155
|
-
#
|
156
|
-
# @param inner [Hash] Attributes for the inner wrapper.
|
157
|
-
# @param attributes [Hash] Additional attributes for the wrapper.
|
158
|
-
# @yield [block] The block to be executed within the wrapper.
|
159
|
-
# @return [Components::Wrapper] The wrapper component.
|
160
|
-
def wrapped(inner: {}, **attributes, &)
|
161
|
-
wrapper_class = attributes.delete(:class) || themed(attributes.delete(:theme) || :wrapper)
|
162
|
-
inner[:class] = inner.delete(:class) || themed(inner.delete(:theme) || :inner_wrapper)
|
163
|
-
Components::Wrapper.new(self, class: wrapper_class, inner: inner, **attributes, &)
|
164
|
-
end
|
165
|
-
|
166
|
-
# Creates a multi-value field collection.
|
167
|
-
#
|
168
|
-
# @param range [Integer, #to_a] The range of keys for each field. If an integer is passed, keys will begin from 1.
|
169
|
-
# @yield [block] The block to be executed for each item in the collection.
|
170
|
-
# @return [FieldCollection] The field collection.
|
171
|
-
def multi(range = nil, &)
|
172
|
-
FieldCollection.new(field: self, range: range, &)
|
173
|
-
end
|
174
|
-
|
175
|
-
# Creates a submit button
|
176
|
-
#
|
177
|
-
# @param attributes [Hash] Additional attributes for the submit.
|
178
|
-
# @return [Components::SubmitButton] The submit button component.
|
179
|
-
def submit_button_tag(**attributes, &)
|
180
|
-
create_component(Components::SubmitButton, :submit_button, **attributes, &)
|
181
|
-
end
|
182
|
-
|
183
|
-
def extract_input(params)
|
184
|
-
raise "field##{dom.name} did not define an input component" unless @field_input_component
|
185
|
-
|
186
|
-
@field_input_component.extract_input(params)
|
187
|
-
end
|
188
|
-
|
189
|
-
protected
|
190
|
-
|
191
|
-
def create_component(component_class, theme_key, **attributes, &)
|
192
|
-
if component_class.include?(Phlexi::Display::Components::Concerns::HandlesInput)
|
193
|
-
raise "input component already defined: #{@field_input_component.inspect}" if @field_input_component
|
194
|
-
|
195
|
-
attributes = input_attributes.deep_merge(attributes)
|
196
|
-
@field_input_component = component_class.new(self, class: component_class_for(theme_key, attributes), **attributes, &)
|
197
|
-
else
|
198
|
-
component_class.new(self, class: component_class_for(theme_key, attributes), **attributes, &)
|
199
|
-
end
|
200
|
-
end
|
201
|
-
|
202
|
-
def component_class_for(theme_key, attributes)
|
203
|
-
attributes.delete(:class) || themed(attributes.key?(:theme) ? attributes.delete(:theme) : theme_key)
|
204
|
-
end
|
205
|
-
|
206
|
-
def has_value?
|
207
|
-
value.present?
|
208
|
-
end
|
209
|
-
|
210
|
-
def determine_initial_value(value)
|
211
|
-
return value unless value == NIL_VALUE
|
212
|
-
|
213
|
-
determine_from_association || determine_value_from_object
|
214
|
-
end
|
215
|
-
|
216
|
-
def determine_value_from_object
|
217
|
-
object.respond_to?(key) ? object.public_send(key) : nil
|
218
|
-
end
|
219
|
-
|
220
|
-
def determine_from_association
|
221
|
-
return nil unless reflection.present?
|
222
|
-
|
223
|
-
value = object.public_send(key)
|
224
|
-
case reflection.macro
|
225
|
-
when :has_many, :has_and_belongs_to_many
|
226
|
-
value&.map { |v| v.public_send(reflection.klass.primary_key) }
|
227
|
-
when :belongs_to, :has_one
|
228
|
-
value&.public_send(reflection.klass.primary_key)
|
229
|
-
else
|
230
|
-
raise ArgumentError, "Unsupported association type: #{reflection.macro}"
|
231
|
-
end
|
232
|
-
end
|
233
|
-
end
|
234
|
-
end
|
235
|
-
end
|
236
|
-
end
|
@@ -1,54 +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, input_attributes: @field.input_attributes, **, parent: @field).tap do |field|
|
20
|
-
yield field if block_given?
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def hidden_field_tag(value: "", force: false)
|
25
|
-
raise "Attempting to build hidden field on non-first field in a collection" unless index == 0 || force
|
26
|
-
|
27
|
-
@field.class
|
28
|
-
.new("hidden", parent: @field)
|
29
|
-
.input_tag(type: :hidden, value:)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def initialize(field:, range:, &)
|
34
|
-
@field = field
|
35
|
-
@range = case range
|
36
|
-
when Range, Array
|
37
|
-
range
|
38
|
-
when Integer
|
39
|
-
1..range
|
40
|
-
else
|
41
|
-
range.to_a
|
42
|
-
end
|
43
|
-
each(&) if block_given?
|
44
|
-
end
|
45
|
-
|
46
|
-
def each(&)
|
47
|
-
@range.each.with_index do |key, index|
|
48
|
-
yield Builder.new(key, @field, index)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
@@ -1,135 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Phlexi
|
4
|
-
module Display
|
5
|
-
module Structure
|
6
|
-
# A Namespace maps and 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
|
-
# To access the values on a Namespace, the `field` can be called for single values.
|
9
|
-
#
|
10
|
-
# Additionally, to access namespaces within a namespace, such as if a `User has_many :addresses` in
|
11
|
-
# ActiveRecord, the `namespace` method can be called which will return another Namespace object and
|
12
|
-
# set the current Namespace as the parent.
|
13
|
-
class Namespace < Structure::Node
|
14
|
-
include Enumerable
|
15
|
-
|
16
|
-
attr_reader :builder_klass, :object
|
17
|
-
|
18
|
-
def initialize(key, parent:, builder_klass:, object: nil)
|
19
|
-
super(key, parent: parent)
|
20
|
-
@builder_klass = builder_klass
|
21
|
-
@object = object
|
22
|
-
@children = {}
|
23
|
-
yield self if block_given?
|
24
|
-
end
|
25
|
-
|
26
|
-
def field(key, **attributes)
|
27
|
-
create_child(key, attributes.delete(:builder_klass) || builder_klass, object: object, **attributes).tap do |field|
|
28
|
-
yield field if block_given?
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def submit_button(key = nil, **attributes, &)
|
33
|
-
field(key || SecureRandom.hex).submit_button_tag(**attributes, &)
|
34
|
-
end
|
35
|
-
|
36
|
-
# Creates a `Namespace` child instance with the parent set to the current instance, adds to
|
37
|
-
# the `@children` Hash to ensure duplicate child namespaces aren't created, then calls the
|
38
|
-
# method on the `@object` to get the child object to pass into that namespace.
|
39
|
-
#
|
40
|
-
# For example, if a `User#permission` returns a `Permission` object, we could map that to a
|
41
|
-
# form like this:
|
42
|
-
#
|
43
|
-
# ```ruby
|
44
|
-
# Superform :user, object: User.new do |form|
|
45
|
-
# form.nest_one :permission do |permission|
|
46
|
-
# form.field :role
|
47
|
-
# end
|
48
|
-
# end
|
49
|
-
# ```
|
50
|
-
def nest_one(key, object: nil, &)
|
51
|
-
object ||= object_value_for(key: key)
|
52
|
-
create_child(key, self.class, object:, builder_klass:, &)
|
53
|
-
end
|
54
|
-
|
55
|
-
# Wraps an array of objects in Namespace classes. For example, if `User#addresses` returns
|
56
|
-
# an enumerable or array of `Address` classes:
|
57
|
-
#
|
58
|
-
# ```ruby
|
59
|
-
# Phlexi::Display.new User.new do |form|
|
60
|
-
# render form.field(:email).input_tag
|
61
|
-
# render form.field(:name).input_tag
|
62
|
-
# form.nest_many :addresses do |address|
|
63
|
-
# render address.field(:street).input_tag
|
64
|
-
# render address.field(:state).input_tag
|
65
|
-
# render address.field(:zip).input_tag
|
66
|
-
# end
|
67
|
-
# end
|
68
|
-
# ```
|
69
|
-
# The object within the block is a `Namespace` object that maps each object within the enumerable
|
70
|
-
# to another `Namespace` or `Field`.
|
71
|
-
def nest_many(key, collection: nil, &)
|
72
|
-
collection ||= Array(object_value_for(key: key))
|
73
|
-
create_child(key, NamespaceCollection, collection:, &)
|
74
|
-
end
|
75
|
-
|
76
|
-
def extract_input(params)
|
77
|
-
if params.is_a?(Array)
|
78
|
-
each_with_object({}) do |child, hash|
|
79
|
-
hash.merge! child.extract_input(params[0])
|
80
|
-
end
|
81
|
-
else
|
82
|
-
input = each_with_object({}) do |child, hash|
|
83
|
-
hash.merge! child.extract_input(params[key])
|
84
|
-
end
|
85
|
-
{key => input}
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
# Iterates through the children of the current namespace, which could be `Namespace` or `Field`
|
90
|
-
# objects.
|
91
|
-
def each(&)
|
92
|
-
@children.values.each(&)
|
93
|
-
end
|
94
|
-
|
95
|
-
def dom_id
|
96
|
-
@dom_id ||= begin
|
97
|
-
id = if object.nil?
|
98
|
-
nil
|
99
|
-
elsif object.class.respond_to?(:primary_key)
|
100
|
-
object.public_send(object.class.primary_key) || :new
|
101
|
-
elsif object.respond_to?(:id)
|
102
|
-
object.id || :new
|
103
|
-
end
|
104
|
-
[key, id].compact.join("_").underscore
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
# Creates a root Namespace, which is essentially a form.
|
109
|
-
def self.root(*, builder_klass:, **, &)
|
110
|
-
new(*, parent: nil, builder_klass:, **, &)
|
111
|
-
end
|
112
|
-
|
113
|
-
protected
|
114
|
-
|
115
|
-
# Calls the corresponding method on the object for the `key` name, if it exists. For example
|
116
|
-
# if the `key` is `email` on `User`, this method would call `User#email` if the method is
|
117
|
-
# present.
|
118
|
-
#
|
119
|
-
# This method could be overwritten if the mapping between the `@object` and `key` name is not
|
120
|
-
# a method call. For example, a `Hash` would be accessed via `user[:email]` instead of `user.send(:email)`
|
121
|
-
def object_value_for(key:)
|
122
|
-
@object.send(key) if @object.respond_to? key
|
123
|
-
end
|
124
|
-
|
125
|
-
private
|
126
|
-
|
127
|
-
# Checks if the child exists. If it does then it returns that. If it doesn't, it will
|
128
|
-
# build the child.
|
129
|
-
def create_child(key, child_class, **kwargs, &block)
|
130
|
-
@children.fetch(key) { @children[key] = child_class.new(key, parent: self, **kwargs, &block) }
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
@@ -1,48 +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
|
-
def extract_input(params)
|
20
|
-
namespace = build_namespace(0)
|
21
|
-
@block.call(namespace)
|
22
|
-
|
23
|
-
inputs = params[key].map { |param| namespace.extract_input([param]) }
|
24
|
-
{key => inputs}
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def each(&)
|
30
|
-
namespaces.each(&)
|
31
|
-
end
|
32
|
-
|
33
|
-
# Builds and memoizes namespaces for the collection.
|
34
|
-
#
|
35
|
-
# @return [Array<Hash>] An array of namespace hashes.
|
36
|
-
def namespaces
|
37
|
-
@namespaces ||= @collection.map.with_index do |object, key|
|
38
|
-
build_namespace(key, object: object)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
def build_namespace(index, **)
|
43
|
-
parent.class.new(index, parent: self, builder_klass: parent.builder_klass, **)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|