phlexi-form 0.2.0 → 0.3.0.rc1
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/Appraisals +4 -9
- data/README.md +115 -316
- data/TODO +4 -0
- data/config.ru +0 -3
- data/gemfiles/default.gemfile.lock +22 -2
- data/gemfiles/rails_7.gemfile +8 -0
- data/gemfiles/rails_7.gemfile.lock +282 -0
- data/lib/phlexi/form/base.rb +52 -35
- data/lib/phlexi/form/components/base.rb +12 -6
- data/lib/phlexi/form/components/checkbox.rb +5 -0
- data/lib/phlexi/form/components/collection_checkboxes.rb +28 -14
- data/lib/phlexi/form/components/collection_radio_buttons.rb +19 -13
- data/lib/phlexi/form/components/concerns/handles_array_input.rb +21 -0
- data/lib/phlexi/form/components/concerns/handles_input.rb +53 -0
- data/lib/phlexi/form/components/concerns/has_options.rb +6 -2
- data/lib/phlexi/form/components/concerns/submits_form.rb +39 -0
- data/lib/phlexi/form/components/file_input.rb +32 -0
- data/lib/phlexi/form/components/input.rb +39 -33
- data/lib/phlexi/form/components/input_array.rb +45 -0
- data/lib/phlexi/form/components/label.rb +2 -1
- data/lib/phlexi/form/components/radio_button.rb +11 -1
- data/lib/phlexi/form/components/select.rb +21 -5
- data/lib/phlexi/form/components/submit_button.rb +41 -0
- data/lib/phlexi/form/field_options/associations.rb +21 -0
- data/lib/phlexi/form/field_options/autofocus.rb +1 -1
- data/lib/phlexi/form/field_options/collection.rb +26 -9
- data/lib/phlexi/form/field_options/errors.rb +10 -0
- data/lib/phlexi/form/field_options/{type.rb → inferred_types.rb} +12 -12
- data/lib/phlexi/form/field_options/multiple.rb +2 -0
- data/lib/phlexi/form/field_options/themes.rb +207 -0
- data/lib/phlexi/form/option_mapper.rb +2 -2
- data/lib/phlexi/form/structure/dom.rb +19 -14
- data/lib/phlexi/form/structure/field_builder.rb +145 -108
- data/lib/phlexi/form/structure/field_collection.rb +14 -5
- data/lib/phlexi/form/structure/namespace.rb +31 -19
- data/lib/phlexi/form/structure/namespace_collection.rb +20 -20
- data/lib/phlexi/form/structure/node.rb +1 -1
- data/lib/phlexi/form/version.rb +1 -1
- data/lib/phlexi/form.rb +4 -1
- metadata +30 -6
- data/CODE_OF_CONDUCT.md +0 -84
@@ -19,13 +19,17 @@ module Phlexi
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def selected?(option)
|
22
|
-
if
|
22
|
+
if attributes[:multiple]
|
23
23
|
@options_list ||= Array(field.value)
|
24
24
|
@options_list.any? { |item| item.to_s == option.to_s }
|
25
25
|
else
|
26
|
-
field.
|
26
|
+
field.value.to_s == option.to_s
|
27
27
|
end
|
28
28
|
end
|
29
|
+
|
30
|
+
def normalize_simple_input(input_value)
|
31
|
+
([super] & option_mapper.values)[0]
|
32
|
+
end
|
29
33
|
end
|
30
34
|
end
|
31
35
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Phlexi
|
4
|
+
module Form
|
5
|
+
module Components
|
6
|
+
module Concerns
|
7
|
+
module SubmitsForm
|
8
|
+
def submit_type_value
|
9
|
+
if field.object.respond_to?(:persisted?)
|
10
|
+
field.object.persisted? ? :update : :create
|
11
|
+
else
|
12
|
+
:submit
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def submit_type_label
|
17
|
+
@submit_type_label ||= begin
|
18
|
+
key = submit_type_value
|
19
|
+
|
20
|
+
model_object = field.dom.lineage.first.key.to_s
|
21
|
+
model_name_human = if field.object.respond_to?(:model_name)
|
22
|
+
field.object.model_name.human
|
23
|
+
else
|
24
|
+
model_object.humanize
|
25
|
+
end
|
26
|
+
|
27
|
+
defaults = []
|
28
|
+
defaults << :"helpers.submit.#{model_object}.#{key}"
|
29
|
+
defaults << :"helpers.submit.#{key}"
|
30
|
+
defaults << "#{key.to_s.humanize} #{model_name_human}"
|
31
|
+
|
32
|
+
I18n.t(defaults.shift, model: model_name_human, default: defaults)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Phlexi
|
4
|
+
module Form
|
5
|
+
module Components
|
6
|
+
class FileInput < Input
|
7
|
+
def view_template
|
8
|
+
input(type: :hidden, name: attributes[:name], value: "", autocomplete: "off", hidden: true) if include_hidden?
|
9
|
+
input(**attributes)
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def build_input_attributes
|
15
|
+
attributes[:type] = :file
|
16
|
+
super
|
17
|
+
attributes[:value] = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def include_hidden?
|
21
|
+
return false if @include_hidden == false
|
22
|
+
|
23
|
+
attributes[:multiple]
|
24
|
+
end
|
25
|
+
|
26
|
+
def normalize_input(input_value)
|
27
|
+
input_value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -4,6 +4,8 @@ module Phlexi
|
|
4
4
|
module Form
|
5
5
|
module Components
|
6
6
|
class Input < Base
|
7
|
+
include Concerns::HandlesInput
|
8
|
+
|
7
9
|
def view_template
|
8
10
|
input(**attributes)
|
9
11
|
end
|
@@ -15,6 +17,7 @@ module Phlexi
|
|
15
17
|
|
16
18
|
# only overwrite id if it was set in Base
|
17
19
|
attributes[:id] = field.dom.id if attributes[:id] == "#{field.dom.id}_#{component_name}"
|
20
|
+
|
18
21
|
attributes[:name] = field.dom.name
|
19
22
|
attributes[:value] = field.dom.value
|
20
23
|
|
@@ -22,54 +25,57 @@ module Phlexi
|
|
22
25
|
end
|
23
26
|
|
24
27
|
def build_input_attributes
|
25
|
-
attributes
|
26
|
-
attributes
|
28
|
+
attributes.fetch(:type) { attributes[:type] = field.inferred_input_type }
|
29
|
+
attributes.fetch(:disabled) { attributes[:disabled] = field.disabled? }
|
27
30
|
|
28
31
|
case attributes[:type]
|
29
32
|
when :text, :password, :email, :tel, :url, :search
|
30
|
-
attributes
|
31
|
-
attributes
|
32
|
-
attributes
|
33
|
-
attributes
|
34
|
-
attributes
|
35
|
-
attributes
|
36
|
-
attributes
|
33
|
+
attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
|
34
|
+
attributes.fetch(:placeholder) { attributes[:placeholder] = field.placeholder }
|
35
|
+
attributes.fetch(:minlength) { attributes[:minlength] = field.minlength }
|
36
|
+
attributes.fetch(:maxlength) { attributes[:maxlength] = field.maxlength }
|
37
|
+
attributes.fetch(:readonly) { attributes[:readonly] = field.readonly? }
|
38
|
+
attributes.fetch(:required) { attributes[:required] = field.required? }
|
39
|
+
attributes.fetch(:pattern) { attributes[:pattern] = field.pattern }
|
37
40
|
when :number
|
38
|
-
attributes
|
39
|
-
attributes
|
40
|
-
attributes
|
41
|
-
attributes
|
42
|
-
attributes
|
43
|
-
attributes
|
44
|
-
attributes
|
41
|
+
attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
|
42
|
+
attributes.fetch(:placeholder) { attributes[:placeholder] = field.placeholder }
|
43
|
+
attributes.fetch(:readonly) { attributes[:readonly] = field.readonly? }
|
44
|
+
attributes.fetch(:required) { attributes[:required] = field.required? }
|
45
|
+
attributes.fetch(:min) { attributes[:min] = field.min }
|
46
|
+
attributes.fetch(:max) { attributes[:max] = field.max }
|
47
|
+
attributes.fetch(:step) { attributes[:step] = field.step }
|
45
48
|
when :checkbox, :radio
|
46
|
-
attributes
|
47
|
-
attributes
|
49
|
+
attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
|
50
|
+
attributes.fetch(:required) { attributes[:required] = field.required? }
|
48
51
|
when :file
|
49
|
-
attributes
|
50
|
-
attributes
|
51
|
-
attributes
|
52
|
-
attributes[:accept] = attributes.fetch(:accept, field.accept)
|
52
|
+
attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
|
53
|
+
attributes.fetch(:required) { attributes[:required] = field.required? }
|
54
|
+
attributes.fetch(:multiple) { attributes[:multiple] = field.multiple? }
|
53
55
|
when :date, :time, :datetime_local
|
54
|
-
attributes
|
55
|
-
attributes
|
56
|
-
attributes
|
57
|
-
attributes
|
58
|
-
attributes
|
56
|
+
attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
|
57
|
+
attributes.fetch(:readonly) { attributes[:readonly] = field.readonly? }
|
58
|
+
attributes.fetch(:required) { attributes[:required] = field.required? }
|
59
|
+
attributes.fetch(:min) { attributes[:min] = field.min }
|
60
|
+
attributes.fetch(:max) { attributes[:max] = field.max }
|
59
61
|
when :color
|
60
|
-
attributes
|
62
|
+
attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
|
61
63
|
when :range
|
62
|
-
attributes
|
63
|
-
attributes
|
64
|
-
attributes
|
65
|
-
attributes
|
64
|
+
attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
|
65
|
+
attributes.fetch(:min) { attributes[:min] = field.min }
|
66
|
+
attributes.fetch(:max) { attributes[:max] = field.max }
|
67
|
+
attributes.fetch(:step) { attributes[:step] = field.step }
|
68
|
+
when :hidden
|
69
|
+
attributes[:class] = false
|
70
|
+
attributes[:hidden] = true
|
71
|
+
attributes[:autocomplete] = "off"
|
66
72
|
else
|
67
73
|
# Handle any unrecognized input types
|
68
74
|
# Rails.logger.warn("Unhandled input type: #{attributes[:type]}")
|
69
75
|
end
|
70
76
|
|
71
77
|
if (attributes[:type] == :file) ? attributes[:multiple] : attributes.delete(:multiple)
|
72
|
-
attributes[:name] = "#{attributes[:name].sub(/\[]$/, "")}[]"
|
78
|
+
attributes[:name] = "#{attributes[:name].sub(/\[\]$/, "")}[]"
|
73
79
|
end
|
74
80
|
end
|
75
81
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Phlexi
|
4
|
+
module Form
|
5
|
+
module Components
|
6
|
+
class InputArray < Base
|
7
|
+
include Concerns::HandlesInput
|
8
|
+
include Concerns::HandlesArrayInput
|
9
|
+
|
10
|
+
def view_template
|
11
|
+
div(**attributes.slice(:id, :class)) do
|
12
|
+
field.multi(values.length) do |builder|
|
13
|
+
render builder.hidden_field_tag if builder.index == 0
|
14
|
+
|
15
|
+
field = builder.field(
|
16
|
+
label: builder.key,
|
17
|
+
# we expect key to be an integer string starting from "1"
|
18
|
+
value: values[builder.index]
|
19
|
+
)
|
20
|
+
if block_given?
|
21
|
+
yield field
|
22
|
+
else
|
23
|
+
render field.input_tag
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def build_attributes
|
32
|
+
super
|
33
|
+
|
34
|
+
attributes[:multiple] = true
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def values
|
40
|
+
@values ||= Array(field.value)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -8,6 +8,11 @@ module Phlexi
|
|
8
8
|
input(**attributes, value: @checked_value)
|
9
9
|
end
|
10
10
|
|
11
|
+
def extract_input(...)
|
12
|
+
# when a radio is not submitted, nothing is returned
|
13
|
+
super.compact
|
14
|
+
end
|
15
|
+
|
11
16
|
protected
|
12
17
|
|
13
18
|
def build_input_attributes
|
@@ -17,7 +22,7 @@ module Phlexi
|
|
17
22
|
@checked_value = (attributes.key?(:checked_value) ? attributes.delete(:checked_value) : "1").to_s
|
18
23
|
|
19
24
|
# this is a hack to workaround the fact that radio cannot be indexed/multiple
|
20
|
-
attributes[:name] = attributes[:name].sub(/\[]$/, "")
|
25
|
+
attributes[:name] = attributes[:name].sub(/\[\]$/, "")
|
21
26
|
attributes[:value] = @checked_value
|
22
27
|
attributes[:checked] = attributes.fetch(:checked) { checked? }
|
23
28
|
end
|
@@ -25,6 +30,11 @@ module Phlexi
|
|
25
30
|
def checked?
|
26
31
|
field.dom.value == @checked_value
|
27
32
|
end
|
33
|
+
|
34
|
+
def normalize_input(...)
|
35
|
+
input_value = super
|
36
|
+
(input_value == @checked_value) ? input_value : nil
|
37
|
+
end
|
28
38
|
end
|
29
39
|
end
|
30
40
|
end
|
@@ -4,11 +4,14 @@ module Phlexi
|
|
4
4
|
module Form
|
5
5
|
module Components
|
6
6
|
class Select < Base
|
7
|
+
include Concerns::HandlesInput
|
8
|
+
include Concerns::HandlesArrayInput
|
7
9
|
include Concerns::HasOptions
|
8
10
|
|
9
|
-
def view_template
|
11
|
+
def view_template
|
12
|
+
input(type: :hidden, name: attributes[:name], value: "", autocomplete: "off", hidden: true) if include_hidden?
|
10
13
|
select(**attributes) do
|
11
|
-
blank_option { blank_option_text }
|
14
|
+
blank_option { blank_option_text } if include_blank?
|
12
15
|
options
|
13
16
|
end
|
14
17
|
end
|
@@ -35,7 +38,8 @@ module Phlexi
|
|
35
38
|
end
|
36
39
|
|
37
40
|
def build_select_attributes
|
38
|
-
@
|
41
|
+
@include_blank = attributes.delete(:include_blank)
|
42
|
+
@include_hidden = attributes.delete(:include_hidden)
|
39
43
|
|
40
44
|
attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
|
41
45
|
attributes[:required] = attributes.fetch(:required, field.required?)
|
@@ -48,8 +52,20 @@ module Phlexi
|
|
48
52
|
field.placeholder
|
49
53
|
end
|
50
54
|
|
51
|
-
def
|
52
|
-
@
|
55
|
+
def include_blank?
|
56
|
+
return true if @include_blank == true
|
57
|
+
|
58
|
+
@include_blank != false && !attributes[:multiple]
|
59
|
+
end
|
60
|
+
|
61
|
+
def include_hidden?
|
62
|
+
return false if @include_hidden == false
|
63
|
+
|
64
|
+
attributes[:multiple]
|
65
|
+
end
|
66
|
+
|
67
|
+
def normalize_input(input_value)
|
68
|
+
attributes[:multiple] ? normalize_array_input(input_value) : normalize_simple_input(input_value)
|
53
69
|
end
|
54
70
|
end
|
55
71
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Phlexi
|
4
|
+
module Form
|
5
|
+
module Components
|
6
|
+
class SubmitButton < Base
|
7
|
+
include Concerns::SubmitsForm
|
8
|
+
|
9
|
+
def view_template(&content)
|
10
|
+
content ||= proc { submit_type_label }
|
11
|
+
button(**attributes, &content)
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def build_attributes
|
17
|
+
root_key = field.dom.lineage.first.respond_to?(:dom_id) ? field.dom.lineage.first.dom_id : field.dom.lineage.first.key
|
18
|
+
attributes.fetch(:id) { attributes[:id] = "#{root_key}_submit_button" }
|
19
|
+
attributes[:class] = tokens(
|
20
|
+
component_name,
|
21
|
+
submit_type_value,
|
22
|
+
attributes[:class]
|
23
|
+
)
|
24
|
+
|
25
|
+
build_button_attributes
|
26
|
+
end
|
27
|
+
|
28
|
+
def build_button_attributes
|
29
|
+
formmethod = attributes[:formmethod]
|
30
|
+
if formmethod.present? && !/post|get/i.match?(formmethod) && !attributes.key?(:name) && !attributes.key?(:value)
|
31
|
+
attributes.merge! formmethod: :post, name: "_method", value: formmethod
|
32
|
+
end
|
33
|
+
|
34
|
+
attributes.fetch(:name) { attributes[:name] = "commit" }
|
35
|
+
attributes.fetch(:value) { attributes[:value] = submit_type_label }
|
36
|
+
attributes.fetch(:type) { attributes[:type] = :submit }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Phlexi
|
4
|
+
module Form
|
5
|
+
module FieldOptions
|
6
|
+
module Associations
|
7
|
+
protected
|
8
|
+
|
9
|
+
def reflection
|
10
|
+
@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
|
@@ -16,20 +16,37 @@ module Phlexi
|
|
16
16
|
private
|
17
17
|
|
18
18
|
def infer_collection
|
19
|
-
|
20
|
-
inclusion_validator = find_inclusion_validator
|
21
|
-
collection_value_from(inclusion_validator)
|
22
|
-
end
|
19
|
+
collection_value_from_association || collection_value_from_validator
|
23
20
|
end
|
24
21
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
22
|
+
def collection_value_from_association
|
23
|
+
return unless reflection
|
24
|
+
|
25
|
+
relation = reflection.klass.all
|
26
|
+
|
27
|
+
if reflection.respond_to?(:scope) && reflection.scope
|
28
|
+
relation = if reflection.scope.parameters.any?
|
29
|
+
reflection.klass.instance_exec(object, &reflection.scope)
|
30
|
+
else
|
31
|
+
reflection.klass.instance_exec(&reflection.scope)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
order = reflection.options[:order]
|
35
|
+
conditions = reflection.options[:conditions]
|
36
|
+
conditions = object.instance_exec(&conditions) if conditions.respond_to?(:call)
|
37
|
+
|
38
|
+
relation = relation.where(conditions) if relation.respond_to?(:where) && conditions.present?
|
39
|
+
relation = relation.order(order) if relation.respond_to?(:order)
|
28
40
|
end
|
41
|
+
|
42
|
+
relation
|
29
43
|
end
|
30
44
|
|
31
|
-
def
|
32
|
-
|
45
|
+
def collection_value_from_validator
|
46
|
+
return unless has_validators?
|
47
|
+
|
48
|
+
inclusion_validator = find_validator(:inclusion)
|
49
|
+
inclusion_validator.options[:in] || inclusion_validator.options[:within] if inclusion_validator
|
33
50
|
end
|
34
51
|
end
|
35
52
|
end
|
@@ -76,6 +76,16 @@ module Phlexi
|
|
76
76
|
def has_custom_error?
|
77
77
|
options[:error].is_a?(String)
|
78
78
|
end
|
79
|
+
|
80
|
+
# Determines if the associated object is in a valid state
|
81
|
+
#
|
82
|
+
# An object is considered valid if it is persisted and has no errors.
|
83
|
+
#
|
84
|
+
# @return [Boolean] true if the object is persisted and has no errors, false otherwise
|
85
|
+
def object_valid?
|
86
|
+
object.respond_to?(:persisted?) && object.persisted? &&
|
87
|
+
object.respond_to?(:errors) && !object.errors.empty?
|
88
|
+
end
|
79
89
|
end
|
80
90
|
end
|
81
91
|
end
|
@@ -1,19 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "bigdecimal"
|
4
|
+
|
3
5
|
module Phlexi
|
4
6
|
module Form
|
5
7
|
module FieldOptions
|
6
|
-
module
|
7
|
-
def
|
8
|
-
@
|
8
|
+
module InferredTypes
|
9
|
+
def inferred_db_type
|
10
|
+
@inferred_db_type ||= infer_db_type
|
9
11
|
end
|
10
12
|
|
11
|
-
def
|
12
|
-
@
|
13
|
+
def inferred_input_component
|
14
|
+
@inferred_input_component ||= infer_input_component
|
13
15
|
end
|
14
16
|
|
15
|
-
def
|
16
|
-
@
|
17
|
+
def inferred_input_type
|
18
|
+
@inferred_input_type ||= infer_input_type(inferred_input_component)
|
17
19
|
end
|
18
20
|
|
19
21
|
private
|
@@ -23,7 +25,7 @@ module Phlexi
|
|
23
25
|
def infer_input_component
|
24
26
|
return :select unless collection.blank?
|
25
27
|
|
26
|
-
case
|
28
|
+
case inferred_db_type
|
27
29
|
when :text, :json, :jsonb, :hstore
|
28
30
|
:textarea
|
29
31
|
else
|
@@ -33,10 +35,8 @@ module Phlexi
|
|
33
35
|
|
34
36
|
# this only applies when input_component is `:input`
|
35
37
|
# resolves the type attribute of input components
|
36
|
-
def infer_input_type
|
37
|
-
|
38
|
-
|
39
|
-
case db_type
|
38
|
+
def infer_input_type(component)
|
39
|
+
case inferred_db_type
|
40
40
|
when :string
|
41
41
|
infer_string_input_type(key)
|
42
42
|
when :integer, :float, :decimal
|