phlexi-form 0.2.0 → 0.3.0

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +4 -9
  3. data/README.md +117 -316
  4. data/TODO +4 -0
  5. data/config.ru +0 -3
  6. data/gemfiles/default.gemfile.lock +22 -2
  7. data/gemfiles/rails_7.gemfile +8 -0
  8. data/gemfiles/rails_7.gemfile.lock +282 -0
  9. data/lib/phlexi/form/base.rb +65 -56
  10. data/lib/phlexi/form/components/base.rb +14 -8
  11. data/lib/phlexi/form/components/checkbox.rb +5 -0
  12. data/lib/phlexi/form/components/collection_checkboxes.rb +28 -14
  13. data/lib/phlexi/form/components/collection_radio_buttons.rb +19 -13
  14. data/lib/phlexi/form/components/concerns/extracts_input.rb +53 -0
  15. data/lib/phlexi/form/components/concerns/handles_array_input.rb +21 -0
  16. data/lib/phlexi/form/components/concerns/handles_input.rb +23 -0
  17. data/lib/phlexi/form/components/concerns/has_options.rb +6 -2
  18. data/lib/phlexi/form/components/concerns/submits_form.rb +47 -0
  19. data/lib/phlexi/form/components/error.rb +1 -1
  20. data/lib/phlexi/form/components/file_input.rb +33 -0
  21. data/lib/phlexi/form/components/hint.rb +1 -1
  22. data/lib/phlexi/form/components/input.rb +38 -36
  23. data/lib/phlexi/form/components/input_array.rb +45 -0
  24. data/lib/phlexi/form/components/label.rb +2 -1
  25. data/lib/phlexi/form/components/radio_button.rb +11 -1
  26. data/lib/phlexi/form/components/select.rb +21 -8
  27. data/lib/phlexi/form/components/submit_button.rb +41 -0
  28. data/lib/phlexi/form/components/textarea.rb +2 -3
  29. data/lib/phlexi/form/field_options/associations.rb +21 -0
  30. data/lib/phlexi/form/field_options/autofocus.rb +1 -1
  31. data/lib/phlexi/form/field_options/collection.rb +26 -9
  32. data/lib/phlexi/form/field_options/errors.rb +17 -3
  33. data/lib/phlexi/form/field_options/hints.rb +5 -1
  34. data/lib/phlexi/form/field_options/{type.rb → inferred_types.rb} +21 -17
  35. data/lib/phlexi/form/field_options/multiple.rb +2 -0
  36. data/lib/phlexi/form/field_options/required.rb +1 -1
  37. data/lib/phlexi/form/field_options/themes.rb +207 -0
  38. data/lib/phlexi/form/field_options/validators.rb +2 -2
  39. data/lib/phlexi/form/option_mapper.rb +2 -2
  40. data/lib/phlexi/form/structure/dom.rb +21 -16
  41. data/lib/phlexi/form/structure/field_builder.rb +165 -121
  42. data/lib/phlexi/form/structure/field_collection.rb +20 -6
  43. data/lib/phlexi/form/structure/namespace.rb +48 -31
  44. data/lib/phlexi/form/structure/namespace_collection.rb +20 -20
  45. data/lib/phlexi/form/structure/node.rb +13 -3
  46. data/lib/phlexi/form/version.rb +1 -1
  47. data/lib/phlexi/form.rb +4 -1
  48. metadata +32 -7
  49. data/CODE_OF_CONDUCT.md +0 -84
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ module Concerns
7
+ module HandlesArrayInput
8
+ protected
9
+
10
+ def normalize_input(input_value)
11
+ normalize_array_input(input_value)
12
+ end
13
+
14
+ def normalize_array_input(input_value)
15
+ Array(input_value).map { |nested_input_value| normalize_simple_input(nested_input_value) }.compact
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ module Concerns
7
+ module HandlesInput
8
+ include Phlexi::Form::Components::Concerns::ExtractsInput
9
+
10
+ protected
11
+
12
+ def build_attributes
13
+ super
14
+
15
+ # only overwrite id if it was set in Base
16
+ attributes[:id] = field.dom.id if attributes[:id] == "#{field.dom.id}_#{component_name}"
17
+ attributes[:name] = field.dom.name
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -19,13 +19,17 @@ module Phlexi
19
19
  end
20
20
 
21
21
  def selected?(option)
22
- if field.multiple?
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.dom.value == option.to_s
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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Form
5
+ module Components
6
+ module Concerns
7
+ module SubmitsForm
8
+ include Phlexi::Form::Components::Concerns::ExtractsInput
9
+
10
+ def extract_input(params)
11
+ {}
12
+ end
13
+
14
+ protected
15
+
16
+ def submit_type_value
17
+ if field.object.respond_to?(:persisted?)
18
+ field.object.persisted? ? :update : :create
19
+ else
20
+ :submit
21
+ end
22
+ end
23
+
24
+ def submit_type_label
25
+ @submit_type_label ||= begin
26
+ key = submit_type_value
27
+
28
+ model_object = field.dom.lineage.first.key.to_s
29
+ model_name_human = if field.object.respond_to?(:model_name)
30
+ field.object.model_name.human
31
+ else
32
+ model_object.humanize
33
+ end
34
+
35
+ defaults = []
36
+ defaults << :"helpers.submit.#{model_object}.#{key}"
37
+ defaults << :"helpers.submit.#{key}"
38
+ defaults << "#{key.to_s.humanize} #{model_name_human}"
39
+
40
+ I18n.t(defaults.shift, model: model_name_human, default: defaults)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -13,7 +13,7 @@ module Phlexi
13
13
  private
14
14
 
15
15
  def render?
16
- field.show_errors? && field.has_errors?
16
+ field.show_errors?
17
17
  end
18
18
  end
19
19
  end
@@ -0,0 +1,33 @@
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
+ # ensure we are always setting it to false
18
+ attributes[:value] = false
19
+ end
20
+
21
+ def include_hidden?
22
+ return false if @include_hidden == false
23
+
24
+ attributes[:multiple]
25
+ end
26
+
27
+ def normalize_input(input_value)
28
+ input_value
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -13,7 +13,7 @@ module Phlexi
13
13
  private
14
14
 
15
15
  def render?
16
- field.hint.present? && (!field.show_errors? || !field.has_errors?)
16
+ field.show_hint?
17
17
  end
18
18
  end
19
19
  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
@@ -13,63 +15,63 @@ module Phlexi
13
15
  def build_attributes
14
16
  super
15
17
 
16
- # only overwrite id if it was set in Base
17
- attributes[:id] = field.dom.id if attributes[:id] == "#{field.dom.id}_#{component_name}"
18
- attributes[:name] = field.dom.name
19
18
  attributes[:value] = field.dom.value
20
19
 
21
20
  build_input_attributes
22
21
  end
23
22
 
24
23
  def build_input_attributes
25
- attributes[:type] = attributes.fetch(:type, field.input_type)
26
- attributes[:disabled] = attributes.fetch(:disabled, field.disabled?)
24
+ attributes.fetch(:type) { attributes[:type] = field.inferred_input_component_subtype }
25
+ attributes.fetch(:disabled) { attributes[:disabled] = field.disabled? }
27
26
 
28
27
  case attributes[:type]
29
28
  when :text, :password, :email, :tel, :url, :search
30
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
31
- attributes[:placeholder] = attributes.fetch(:placeholder, field.placeholder)
32
- attributes[:minlength] = attributes.fetch(:minlength, field.minlength)
33
- attributes[:maxlength] = attributes.fetch(:maxlength, field.maxlength)
34
- attributes[:readonly] = attributes.fetch(:readonly, field.readonly?)
35
- attributes[:required] = attributes.fetch(:required, field.required?)
36
- attributes[:pattern] = attributes.fetch(:pattern, field.pattern)
29
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
30
+ attributes.fetch(:placeholder) { attributes[:placeholder] = field.placeholder }
31
+ attributes.fetch(:minlength) { attributes[:minlength] = field.minlength }
32
+ attributes.fetch(:maxlength) { attributes[:maxlength] = field.maxlength }
33
+ attributes.fetch(:readonly) { attributes[:readonly] = field.readonly? }
34
+ attributes.fetch(:required) { attributes[:required] = field.required? }
35
+ attributes.fetch(:pattern) { attributes[:pattern] = field.pattern }
37
36
  when :number
38
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
39
- attributes[:placeholder] = attributes.fetch(:placeholder, field.placeholder)
40
- attributes[:readonly] = attributes.fetch(:readonly, field.readonly?)
41
- attributes[:required] = attributes.fetch(:required, field.required?)
42
- attributes[:min] = attributes.fetch(:min, field.min)
43
- attributes[:max] = attributes.fetch(:max, field.max)
44
- attributes[:step] = attributes.fetch(:step, field.step)
37
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
38
+ attributes.fetch(:placeholder) { attributes[:placeholder] = field.placeholder }
39
+ attributes.fetch(:readonly) { attributes[:readonly] = field.readonly? }
40
+ attributes.fetch(:required) { attributes[:required] = field.required? }
41
+ attributes.fetch(:min) { attributes[:min] = field.min }
42
+ attributes.fetch(:max) { attributes[:max] = field.max }
43
+ attributes.fetch(:step) { attributes[:step] = field.step }
45
44
  when :checkbox, :radio
46
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
47
- attributes[:required] = attributes.fetch(:required, field.required?)
45
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
46
+ attributes.fetch(:required) { attributes[:required] = field.required? }
48
47
  when :file
49
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
50
- attributes[:required] = attributes.fetch(:required, field.required?)
51
- attributes[:multiple] = attributes.fetch(:multiple, field.multiple)
52
- attributes[:accept] = attributes.fetch(:accept, field.accept)
48
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
49
+ attributes.fetch(:required) { attributes[:required] = field.required? }
50
+ attributes.fetch(:multiple) { attributes[:multiple] = field.multiple? }
53
51
  when :date, :time, :datetime_local
54
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
55
- attributes[:readonly] = attributes.fetch(:readonly, field.readonly?)
56
- attributes[:required] = attributes.fetch(:required, field.required?)
57
- attributes[:min] = attributes.fetch(:min, field.min)
58
- attributes[:max] = attributes.fetch(:max, field.max)
52
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
53
+ attributes.fetch(:readonly) { attributes[:readonly] = field.readonly? }
54
+ attributes.fetch(:required) { attributes[:required] = field.required? }
55
+ attributes.fetch(:min) { attributes[:min] = field.min }
56
+ attributes.fetch(:max) { attributes[:max] = field.max }
59
57
  when :color
60
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
58
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
61
59
  when :range
62
- attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
63
- attributes[:min] = attributes.fetch(:min, field.min)
64
- attributes[:max] = attributes.fetch(:max, field.max)
65
- attributes[:step] = attributes.fetch(:step, field.step)
60
+ attributes.fetch(:autofocus) { attributes[:autofocus] = field.focused? }
61
+ attributes.fetch(:min) { attributes[:min] = field.min }
62
+ attributes.fetch(:max) { attributes[:max] = field.max }
63
+ attributes.fetch(:step) { attributes[:step] = field.step }
64
+ when :hidden
65
+ attributes[:class] = false
66
+ attributes[:hidden] = true
67
+ attributes[:autocomplete] = "off"
66
68
  else
67
69
  # Handle any unrecognized input types
68
70
  # Rails.logger.warn("Unhandled input type: #{attributes[:type]}")
69
71
  end
70
72
 
71
73
  if (attributes[:type] == :file) ? attributes[:multiple] : attributes.delete(:multiple)
72
- attributes[:name] = "#{attributes[:name].sub(/\[]$/, "")}[]"
74
+ attributes[:name] = "#{attributes[:name].sub(/\[\]$/, "")}[]"
73
75
  end
74
76
  end
75
77
  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.repeated(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
@@ -18,7 +18,8 @@ module Phlexi
18
18
 
19
19
  def build_attributes
20
20
  super
21
- attributes[:for] ||= field.dom.id
21
+
22
+ attributes.fetch(:for) { attributes[:for] = field.dom.id }
22
23
  end
23
24
  end
24
25
  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(&block)
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 } unless skip_blank_option?
14
+ blank_option { blank_option_text } if include_blank?
12
15
  options
13
16
  end
14
17
  end
@@ -28,14 +31,12 @@ module Phlexi
28
31
  def build_attributes
29
32
  super
30
33
 
31
- attributes[:id] = field.dom.id
32
- attributes[:name] = field.dom.name
33
-
34
34
  build_select_attributes
35
35
  end
36
36
 
37
37
  def build_select_attributes
38
- @include_blank_option = attributes.delete(:include_blank_option)
38
+ @include_blank = attributes.delete(:include_blank)
39
+ @include_hidden = attributes.delete(:include_hidden)
39
40
 
40
41
  attributes[:autofocus] = attributes.fetch(:autofocus, field.focused?)
41
42
  attributes[:required] = attributes.fetch(:required, field.required?)
@@ -48,8 +49,20 @@ module Phlexi
48
49
  field.placeholder
49
50
  end
50
51
 
51
- def skip_blank_option?
52
- @include_blank_option == false
52
+ def include_blank?
53
+ return true if @include_blank == true
54
+
55
+ @include_blank != false && !attributes[:multiple]
56
+ end
57
+
58
+ def include_hidden?
59
+ return false if @include_hidden == false
60
+
61
+ attributes[:multiple]
62
+ end
63
+
64
+ def normalize_input(input_value)
65
+ attributes[:multiple] ? normalize_array_input(input_value) : normalize_simple_input(input_value)
53
66
  end
54
67
  end
55
68
  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
@@ -4,6 +4,8 @@ module Phlexi
4
4
  module Form
5
5
  module Components
6
6
  class Textarea < Base
7
+ include Concerns::HandlesInput
8
+
7
9
  def view_template
8
10
  textarea(**attributes) { field.dom.value }
9
11
  end
@@ -13,9 +15,6 @@ module Phlexi
13
15
  def build_attributes
14
16
  super
15
17
 
16
- attributes[:id] = field.dom.id
17
- attributes[:name] = field.dom.name
18
-
19
18
  build_textarea_attributes
20
19
  end
21
20
 
@@ -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 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
@@ -8,7 +8,7 @@ module Phlexi
8
8
  options[:autofocus] == true
9
9
  end
10
10
 
11
- def focus(autofocus = true)
11
+ def focused!(autofocus = true)
12
12
  options[:autofocus] = autofocus
13
13
  self
14
14
  end
@@ -16,20 +16,37 @@ module Phlexi
16
16
  private
17
17
 
18
18
  def infer_collection
19
- if has_validators?
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 collection_value_from(inclusion_validator)
26
- if inclusion_validator
27
- inclusion_validator.options[:in] || inclusion_validator.options[:within]
22
+ def collection_value_from_association
23
+ return unless association_reflection
24
+
25
+ relation = association_reflection.klass.all
26
+
27
+ if association_reflection.respond_to?(:scope) && association_reflection.scope
28
+ relation = if association_reflection.scope.parameters.any?
29
+ association_reflection.klass.instance_exec(object, &association_reflection.scope)
30
+ else
31
+ association_reflection.klass.instance_exec(&association_reflection.scope)
32
+ end
33
+ else
34
+ order = association_reflection.options[:order]
35
+ conditions = association_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 find_inclusion_validator
32
- find_validator(:inclusion)
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
@@ -21,10 +21,14 @@ module Phlexi
21
21
  object_with_errors? || !object && has_custom_error?
22
22
  end
23
23
 
24
- def show_errors?
24
+ def can_show_errors?
25
25
  options[:error] != false
26
26
  end
27
27
 
28
+ def show_errors?
29
+ can_show_errors? && has_errors?
30
+ end
31
+
28
32
  def valid?
29
33
  !has_errors? && has_value?
30
34
  end
@@ -66,16 +70,26 @@ module Phlexi
66
70
  end
67
71
 
68
72
  def errors_on_association
69
- reflection ? object.errors[reflection.name] : []
73
+ association_reflection ? object.errors[association_reflection.name] : []
70
74
  end
71
75
 
72
76
  def full_errors_on_association
73
- reflection ? object.errors.full_messages_for(reflection.name) : []
77
+ association_reflection ? object.errors.full_messages_for(association_reflection.name) : []
74
78
  end
75
79
 
76
80
  def has_custom_error?
77
81
  options[:error].is_a?(String)
78
82
  end
83
+
84
+ # Determines if the associated object is in a valid state
85
+ #
86
+ # An object is considered valid if it is persisted and has no errors.
87
+ #
88
+ # @return [Boolean] true if the object is persisted and has no errors, false otherwise
89
+ def object_valid?
90
+ object.respond_to?(:persisted?) && object.persisted? &&
91
+ object.respond_to?(:errors) && !object.errors.empty?
92
+ end
79
93
  end
80
94
  end
81
95
  end
@@ -14,7 +14,11 @@ module Phlexi
14
14
  end
15
15
 
16
16
  def has_hint?
17
- options[:hint] != false && hint.present?
17
+ hint.present?
18
+ end
19
+
20
+ def show_hint?
21
+ has_hint? && !show_errors?
18
22
  end
19
23
  end
20
24
  end