formatic 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +32 -0
- data/app/assets/javascript/formatic/components/date.ts +54 -0
- data/app/assets/javascript/formatic/components/select.ts +109 -0
- data/app/assets/javascript/formatic/components/stepper.ts +89 -0
- data/app/assets/javascript/formatic/components/textarea.ts +108 -0
- data/app/assets/javascript/formatic/components/toggle.ts +81 -0
- data/app/assets/javascript/formatic.js +350 -0
- data/app/assets/javascript/formatic.js.map +1 -0
- data/app/assets/stylesheets/formatic/components/checklist.sass +3 -0
- data/app/assets/stylesheets/formatic/components/date.sass +70 -0
- data/app/assets/stylesheets/formatic/components/select.sass +24 -0
- data/app/assets/stylesheets/formatic/components/stepper.sass +43 -0
- data/app/assets/stylesheets/formatic/components/string.sass +13 -0
- data/app/assets/stylesheets/formatic/components/textarea.sass +22 -0
- data/app/assets/stylesheets/formatic/components/toggle.sass +93 -0
- data/app/assets/stylesheets/formatic/components/wrapper.sass +51 -0
- data/app/assets/stylesheets/formatic/generics/flip.sass +3 -0
- data/app/assets/stylesheets/formatic/index.sass +16 -0
- data/app/assets/stylesheets/formatic/package.json +5 -0
- data/app/assets/stylesheets/formatic/scopes/form.sass +45 -0
- data/app/assets/stylesheets/formatic/settings/_colors.sass +13 -0
- data/app/assets/stylesheets/formatic/tools/terminal.sass +3 -0
- data/app/assets/stylesheets/formatic/utilities/container.sass +2 -0
- data/app/components/formatic/application_component.rb +39 -0
- data/app/components/formatic/base.rb +72 -0
- data/app/components/formatic/checklist.rb +65 -0
- data/app/components/formatic/date.rb +110 -0
- data/app/components/formatic/select.rb +49 -0
- data/app/components/formatic/stepper.rb +35 -0
- data/app/components/formatic/string.rb +57 -0
- data/app/components/formatic/textarea.rb +48 -0
- data/app/components/formatic/toggle.rb +57 -0
- data/app/components/formatic/wrapper.rb +122 -0
- data/config/locales/formatic.de.yml +5 -0
- data/config/locales/formatic.en.yml +5 -0
- data/lib/formatic/choices/countries.rb +14 -0
- data/lib/formatic/choices/keys.rb +27 -0
- data/lib/formatic/choices/options.rb +39 -0
- data/lib/formatic/choices/records.rb +47 -0
- data/lib/formatic/choices.rb +62 -0
- data/lib/formatic/css.rb +10 -0
- data/lib/formatic/engine.rb +40 -0
- data/lib/formatic/safe_join.rb +20 -0
- data/lib/formatic/templates/date.rb +76 -0
- data/lib/formatic/templates/select.rb +35 -0
- data/lib/formatic/templates/wrapper.rb +52 -0
- data/lib/formatic/version.rb +5 -0
- data/lib/formatic/wrappers/alternative_attribute_name.rb +18 -0
- data/lib/formatic/wrappers/error_messages.rb +39 -0
- data/lib/formatic/wrappers/required.rb +29 -0
- data/lib/formatic/wrappers/translate.rb +41 -0
- data/lib/formatic/wrappers/validators.rb +39 -0
- data/lib/formatic.rb +29 -0
- metadata +186 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'formatic/templates/wrapper'
|
4
|
+
|
5
|
+
module Formatic
|
6
|
+
# Combines label, input, error and hint.
|
7
|
+
# See also https://github.com/rails/rails/blob/main/actionview/lib/action_view/helpers/tags/base.rb
|
8
|
+
class Wrapper < ApplicationComponent
|
9
|
+
# Passing on the form builder.
|
10
|
+
option :f
|
11
|
+
|
12
|
+
# This is not an actual <input>, but it's the wrapper's container div for the <input>.
|
13
|
+
renders_one :input
|
14
|
+
|
15
|
+
# The attribute of the record to be edited. E.g. `:name`.
|
16
|
+
option :attribute_name, type: proc(&:to_sym)
|
17
|
+
|
18
|
+
# Manually decide whether the form field is optional or not.
|
19
|
+
option :required, as: :manual_required, optional: true
|
20
|
+
|
21
|
+
# Manually decide to hide the label.
|
22
|
+
option :label, as: :manual_label, default: -> { true }
|
23
|
+
|
24
|
+
# Manually decide to hide the hint.
|
25
|
+
option :hint, as: :manual_hint, default: -> { true }
|
26
|
+
|
27
|
+
# Autocompletion + Enter should not submit the form.
|
28
|
+
option :prevent_submit_on_enter, default: -> { false }
|
29
|
+
|
30
|
+
# Multiple inputs can belong to one label (e.g. select day, month, year).
|
31
|
+
# With this you can specify the ID of one first input to couple the label to it.
|
32
|
+
option :label_for_id, optional: true
|
33
|
+
|
34
|
+
# CSS
|
35
|
+
option :class, as: :css_class, optional: true
|
36
|
+
|
37
|
+
erb_template(::Formatic::Templates::Wrapper.call)
|
38
|
+
|
39
|
+
# -----------------
|
40
|
+
# Querying of slots
|
41
|
+
# -----------------
|
42
|
+
|
43
|
+
# Whether to display a label or not.
|
44
|
+
def label?
|
45
|
+
manual_label != false
|
46
|
+
end
|
47
|
+
|
48
|
+
# Whether to display a hint or not.
|
49
|
+
def hint?
|
50
|
+
manual_hint != false
|
51
|
+
end
|
52
|
+
|
53
|
+
def error?
|
54
|
+
error_messages.present?
|
55
|
+
end
|
56
|
+
|
57
|
+
def required?
|
58
|
+
@required ||= ::Formatic::Wrappers::Required.call(manual_required:, object:, attribute_name:)
|
59
|
+
end
|
60
|
+
|
61
|
+
def optional?
|
62
|
+
!required?
|
63
|
+
end
|
64
|
+
|
65
|
+
def hint_before_input?
|
66
|
+
manual_hint == :before_input
|
67
|
+
end
|
68
|
+
|
69
|
+
def error_messages
|
70
|
+
@error_messages ||= ::Formatic::Wrappers::ErrorMessages.call(
|
71
|
+
object:,
|
72
|
+
attribute_name:
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
# -----------
|
77
|
+
# Static I18n
|
78
|
+
# -----------
|
79
|
+
|
80
|
+
def placeholder
|
81
|
+
@placeholder ||= ::Formatic::Wrappers::Translate.call(
|
82
|
+
prefix: :'helpers.placeholder',
|
83
|
+
object:,
|
84
|
+
attribute_name:,
|
85
|
+
object_name: f&.object_name
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
def hint
|
90
|
+
@hint ||= ::Formatic::Wrappers::Translate.call(
|
91
|
+
prefix: :'helpers.hint',
|
92
|
+
object:,
|
93
|
+
attribute_name:,
|
94
|
+
object_name: f&.object_name
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
def toggle_on
|
99
|
+
@toggle_on ||= ::Formatic::Wrappers::Translate.call(
|
100
|
+
prefix: :'helpers.hint',
|
101
|
+
object:,
|
102
|
+
attribute_name: :"#{attribute_name}_active",
|
103
|
+
object_name: f&.object_name
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
def toggle_off
|
108
|
+
@toggle_off ||= ::Formatic::Wrappers::Translate.call(
|
109
|
+
prefix: :'helpers.hint',
|
110
|
+
object:,
|
111
|
+
attribute_name: :"#{attribute_name}_inactive",
|
112
|
+
object_name: f&.object_name
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def object
|
119
|
+
f&.object
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formatic
|
4
|
+
class Choices
|
5
|
+
# Returns a list of countries suitable for a <select> box.
|
6
|
+
class Countries
|
7
|
+
include Calls
|
8
|
+
|
9
|
+
def call
|
10
|
+
::ISO3166::Country.pluck(:iso_short_name, :alpha2)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formatic
|
4
|
+
class Choices
|
5
|
+
# Looks up options for a <select> in i18n.
|
6
|
+
class Keys
|
7
|
+
include Calls
|
8
|
+
|
9
|
+
option :f
|
10
|
+
option :attribute_name
|
11
|
+
option :keys
|
12
|
+
|
13
|
+
def call
|
14
|
+
keys.map do |slug|
|
15
|
+
caption = ::Formatic::Wrappers::Translate.call(
|
16
|
+
prefix: :'helpers.options',
|
17
|
+
object: f.object,
|
18
|
+
attribute_name: "#{attribute_name}.#{slug}",
|
19
|
+
object_name: f&.object_name
|
20
|
+
)
|
21
|
+
|
22
|
+
[caption, slug.to_s]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formatic
|
4
|
+
class Choices
|
5
|
+
# Returns raw options suitable for a <select> box.
|
6
|
+
class Options
|
7
|
+
include Calls
|
8
|
+
|
9
|
+
option :f
|
10
|
+
option :options
|
11
|
+
option :attribute_name
|
12
|
+
option :include_current
|
13
|
+
|
14
|
+
def call
|
15
|
+
candidates = options
|
16
|
+
return candidates unless currently_associated_record && include_current
|
17
|
+
return candidates if records&.include?(currently_associated_record)
|
18
|
+
|
19
|
+
candidates.prepend currently_associated_record.presenters.for_select
|
20
|
+
candidates
|
21
|
+
end
|
22
|
+
|
23
|
+
def currently_associated_record
|
24
|
+
return unless association
|
25
|
+
return unless f.object
|
26
|
+
|
27
|
+
f.object.public_send(association.name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def association
|
31
|
+
model_klass = f&.object&.class
|
32
|
+
return false unless model_klass.respond_to?(:reflect_on_all_associations)
|
33
|
+
|
34
|
+
model_klass.reflect_on_all_associations(:belongs_to)
|
35
|
+
.detect { _1.foreign_key == attribute_name.to_s }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formatic
|
4
|
+
class Choices
|
5
|
+
# Returns a list of countries suitable for a <select> box.
|
6
|
+
class Records
|
7
|
+
include Calls
|
8
|
+
|
9
|
+
option :f
|
10
|
+
option :records
|
11
|
+
option :attribute_name
|
12
|
+
option :include_current
|
13
|
+
|
14
|
+
def call
|
15
|
+
candidates = records_to_options || []
|
16
|
+
return candidates unless currently_associated_record && include_current?
|
17
|
+
return candidates if records&.include?(currently_associated_record)
|
18
|
+
|
19
|
+
candidates.prepend currently_associated_record.presenters.for_select
|
20
|
+
candidates
|
21
|
+
end
|
22
|
+
|
23
|
+
def include_current?
|
24
|
+
include_current != false
|
25
|
+
end
|
26
|
+
|
27
|
+
def records_to_options
|
28
|
+
records&.map(&:presenters)&.map(&:for_select)
|
29
|
+
end
|
30
|
+
|
31
|
+
def currently_associated_record
|
32
|
+
return unless association
|
33
|
+
return unless f.object
|
34
|
+
|
35
|
+
f.object.public_send(association.name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def association
|
39
|
+
model_klass = f&.object&.class
|
40
|
+
return false unless model_klass.respond_to?(:reflect_on_all_associations)
|
41
|
+
|
42
|
+
model_klass.reflect_on_all_associations(:belongs_to)
|
43
|
+
.detect { _1.foreign_key == attribute_name.to_s }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formatic
|
4
|
+
# Calculates options for e.g. select boxes.
|
5
|
+
class Choices
|
6
|
+
include Calls
|
7
|
+
|
8
|
+
option :f
|
9
|
+
option :attribute_name
|
10
|
+
option :include_blank, optional: true
|
11
|
+
|
12
|
+
# The currently selected choice should be in the list of choosable choices.
|
13
|
+
# Otherwise a simple form submit would modify this value.
|
14
|
+
option :include_current, optional: true
|
15
|
+
|
16
|
+
option :options, optional: true
|
17
|
+
option :records, optional: true
|
18
|
+
option :keys, optional: true
|
19
|
+
|
20
|
+
def call
|
21
|
+
result = choices
|
22
|
+
result.prepend [nil, nil] if include_blank
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def choices
|
29
|
+
return options_choices if options.present?
|
30
|
+
return country_choices if country_code?
|
31
|
+
return keys_choices if keys.present?
|
32
|
+
|
33
|
+
record_choices
|
34
|
+
end
|
35
|
+
|
36
|
+
def options_choices
|
37
|
+
return options unless include_current
|
38
|
+
|
39
|
+
# Could be implemented though.
|
40
|
+
raise '`Formatic::Choices.call(options: ...)` cannot also have `include_current: true`'
|
41
|
+
end
|
42
|
+
|
43
|
+
def country_code?
|
44
|
+
attribute_name.to_s.end_with?('country_code')
|
45
|
+
end
|
46
|
+
|
47
|
+
# Assuming that countries don't disappear, `include_current` is implied.
|
48
|
+
def country_choices
|
49
|
+
::Formatic::Choices::Countries.call
|
50
|
+
end
|
51
|
+
|
52
|
+
def keys_choices
|
53
|
+
return ::Formatic::Choices::Keys.call(f:, attribute_name:, keys:) unless include_current
|
54
|
+
|
55
|
+
raise '`Formatic::Choices.call(keys: ...)` cannot also have `include_current: true`'
|
56
|
+
end
|
57
|
+
|
58
|
+
def record_choices
|
59
|
+
::Formatic::Choices::Records.call(f:, records:, attribute_name:, include_current:)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/formatic/css.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/engine'
|
4
|
+
require 'dry-initializer'
|
5
|
+
|
6
|
+
module Formatic
|
7
|
+
# :nodoc:
|
8
|
+
class Engine < ::Rails::Engine
|
9
|
+
isolate_namespace Formatic
|
10
|
+
|
11
|
+
config.to_prepare do
|
12
|
+
require_formatic_components
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.require_formatic_components
|
16
|
+
# Our Formatic components are subclasses of `ViewComponent::Base`.
|
17
|
+
# When `ViewComponent::Base` is subclassed, two things happen:
|
18
|
+
#
|
19
|
+
# 1. Rails routes are included into the component
|
20
|
+
# 2. The ViewComponent configuration is accessed
|
21
|
+
#
|
22
|
+
# So we can only require our components, once Rails has booted
|
23
|
+
# AND the view_component gem has been fully initialized (configured).
|
24
|
+
#
|
25
|
+
# That's right here and now.
|
26
|
+
require_relative '../../app/components/formatic/application_component'
|
27
|
+
require_relative '../../app/components/formatic/wrapper'
|
28
|
+
require_relative '../../app/components/formatic/base'
|
29
|
+
|
30
|
+
# Components
|
31
|
+
require_relative '../../app/components/formatic/toggle'
|
32
|
+
require_relative '../../app/components/formatic/checklist'
|
33
|
+
require_relative '../../app/components/formatic/date'
|
34
|
+
require_relative '../../app/components/formatic/select'
|
35
|
+
require_relative '../../app/components/formatic/string'
|
36
|
+
require_relative '../../app/components/formatic/stepper'
|
37
|
+
require_relative '../../app/components/formatic/textarea'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formatic
|
4
|
+
# Joins multiple html safe strings, retaining html safety.
|
5
|
+
class SafeJoin
|
6
|
+
include ::ActionView::Helpers::OutputSafetyHelper
|
7
|
+
|
8
|
+
def self.call(...)
|
9
|
+
new(...).call
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(*input)
|
13
|
+
@input = input
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
safe_join(@input)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formatic
|
4
|
+
module Templates
|
5
|
+
# Holds the ERB template for this component.
|
6
|
+
module Date
|
7
|
+
TEMPLATE = <<~ERB
|
8
|
+
<%= render wrapper do |wrap| %>
|
9
|
+
|
10
|
+
<% wrap.with_input do %>
|
11
|
+
<div class="c-formatic-date s-formatic js-formatic-date">
|
12
|
+
|
13
|
+
<% if readonly %>
|
14
|
+
<div class="s-markdown">
|
15
|
+
<p>
|
16
|
+
<%= value.to_date %>
|
17
|
+
</p>
|
18
|
+
</div>
|
19
|
+
<% else %>
|
20
|
+
|
21
|
+
<div class="c-formatic-date__inputs">
|
22
|
+
<% if discard_day %>
|
23
|
+
= hidden_field_tag day_attribute_name, (day_value || 1)
|
24
|
+
<% else %>
|
25
|
+
<%= select_tag day_attribute_name,
|
26
|
+
options_for_day,
|
27
|
+
id: day_input_id,
|
28
|
+
class: 'c-formatic-date__select js-formatic-date__day' %>
|
29
|
+
|
30
|
+
<%= select_tag month_attribute_name,
|
31
|
+
options_for_month,
|
32
|
+
id: month_input_id,
|
33
|
+
class: 'c-formatic-date__select js-formatic-date__month' %>
|
34
|
+
|
35
|
+
<%= select_tag year_attribute_name,
|
36
|
+
options_for_year,
|
37
|
+
id: year_input_id,
|
38
|
+
class: 'c-formatic-date__select js-formatic-date__year' %>
|
39
|
+
<% end %>
|
40
|
+
</div>
|
41
|
+
<div class="c-formatic-date__calendar">
|
42
|
+
<a class="c-formatic-date__flick c-formatic-date__clear js-formatic-date__shortcut" href="#"
|
43
|
+
data-day=""
|
44
|
+
data-month=""
|
45
|
+
data-year=""
|
46
|
+
>
|
47
|
+
X
|
48
|
+
</a>
|
49
|
+
<% calendar.each do |day| %>
|
50
|
+
<a class="c-formatic-date__flick <%= day.classes %> js-formatic-date__shortcut"
|
51
|
+
href='#'
|
52
|
+
data-day="<%= day.date.day %>"
|
53
|
+
data-month="<%= day.date.month %>"
|
54
|
+
data-year="<%= day.date.year %>"
|
55
|
+
>
|
56
|
+
<span class="c-formatic-date__calendar-day-number"><%= I18n.l(day.date, format: "%e").strip %></span>
|
57
|
+
<span class="c-formatic-date__calendar-month"><%= I18n.l(day.date, format: "%b") %></span>
|
58
|
+
<span class="c-formatic-date__calendar-year"><%= I18n.l(day.date, format: "%y") %></span>
|
59
|
+
</a>
|
60
|
+
<% end %>
|
61
|
+
</div>
|
62
|
+
<% end %>
|
63
|
+
|
64
|
+
</div>
|
65
|
+
<% end %>
|
66
|
+
<% end %>
|
67
|
+
ERB
|
68
|
+
|
69
|
+
private_constant :TEMPLATE
|
70
|
+
|
71
|
+
def self.call
|
72
|
+
TEMPLATE
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formatic
|
4
|
+
module Templates
|
5
|
+
# Holds the ERB template for this component.
|
6
|
+
module Select
|
7
|
+
TEMPLATE = <<~ERB
|
8
|
+
<%= render wrapper do |wrap| %>
|
9
|
+
<% wrap.with_input do %>
|
10
|
+
<div class="c-formatic-select s-formatic">
|
11
|
+
|
12
|
+
<% if readonly %>
|
13
|
+
<div class="s-markdown">
|
14
|
+
<p>
|
15
|
+
<%= current_choice_name %>
|
16
|
+
</p>
|
17
|
+
</div>
|
18
|
+
<% else %>
|
19
|
+
|
20
|
+
<%= f.select attribute_name, choices, {}, { class: ['c-formatic-select', 'js-formatic-select', ('is-autosubmit' if async_submit)] } %>
|
21
|
+
|
22
|
+
<% end %>
|
23
|
+
</div>
|
24
|
+
<% end %>
|
25
|
+
<% end %>
|
26
|
+
ERB
|
27
|
+
|
28
|
+
private_constant :TEMPLATE
|
29
|
+
|
30
|
+
def self.call
|
31
|
+
TEMPLATE
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formatic
|
4
|
+
module Templates
|
5
|
+
# Holds the ERB template for this component.
|
6
|
+
module Wrapper
|
7
|
+
TEMPLATE = <<~ERB
|
8
|
+
<div class="u-formatic-container <%= css_class %>">
|
9
|
+
<div class="c-formatic-wrapper <%= [('is-required' if required?), ('c-formatic-wrapper--hint-before-input' if hint_before_input?)].join(' ') %>">
|
10
|
+
|
11
|
+
<% if label? %>
|
12
|
+
<div class="c-formatic-wrapper__label">
|
13
|
+
<% if label_for_id.present? %>
|
14
|
+
<%= f.label attribute_name, nil, for: label_for_id %>
|
15
|
+
<% else %>
|
16
|
+
<%= f.label attribute_name %>
|
17
|
+
<% end %>
|
18
|
+
</div>
|
19
|
+
<% end %>
|
20
|
+
|
21
|
+
<div class="c-formatic-wrapper__input">
|
22
|
+
<%= input %>
|
23
|
+
</div>
|
24
|
+
|
25
|
+
<% if error? %>
|
26
|
+
<div class="c-formatic-wrapper__error">
|
27
|
+
<i></i>
|
28
|
+
<%= error_messages.to_sentence %><% unless error_messages.to_sentence.end_with?('.') %>.<% end %>
|
29
|
+
</div>
|
30
|
+
<% end %>
|
31
|
+
|
32
|
+
<% if hint? %>
|
33
|
+
<div class="c-formatic-wrapper__hint">
|
34
|
+
<%= hint %>
|
35
|
+
</div>
|
36
|
+
<% end %>
|
37
|
+
|
38
|
+
<% if prevent_submit_on_enter %>
|
39
|
+
<%= f.submit 'Dummy to prevent submit on Enter', disabled: true, class: 'c-formatic-wrapper__prevent-submit-on-enter' %>
|
40
|
+
<% end %>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
ERB
|
44
|
+
|
45
|
+
private_constant :TEMPLATE
|
46
|
+
|
47
|
+
def self.call
|
48
|
+
TEMPLATE
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formatic
|
4
|
+
module Wrappers
|
5
|
+
# Checks if an attribute name is from an associated model.
|
6
|
+
class AlternativeAttributeName
|
7
|
+
include Calls
|
8
|
+
|
9
|
+
param :attribute_name
|
10
|
+
|
11
|
+
def call
|
12
|
+
return unless attribute_name.to_s.end_with?('_id')
|
13
|
+
|
14
|
+
attribute_name[...-3].to_sym
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formatic
|
4
|
+
module Wrappers
|
5
|
+
# Extracts the message of an erroneous attribute
|
6
|
+
class ErrorMessages
|
7
|
+
include Calls
|
8
|
+
|
9
|
+
option :object
|
10
|
+
option :attribute_name
|
11
|
+
|
12
|
+
def call
|
13
|
+
return unless object
|
14
|
+
|
15
|
+
(errors_on_attribute + error_on_association).uniq
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :object
|
21
|
+
|
22
|
+
def errors_on_attribute
|
23
|
+
return [] unless object.respond_to?(attribute_name)
|
24
|
+
|
25
|
+
object.errors.full_messages_for(attribute_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def error_on_association
|
29
|
+
association_attribute_name = ::Formatic::Wrappers::AlternativeAttributeName.call(
|
30
|
+
attribute_name
|
31
|
+
)
|
32
|
+
return [] unless association_attribute_name
|
33
|
+
return [] unless object.respond_to?(association_attribute_name)
|
34
|
+
|
35
|
+
object.errors.full_messages_for(association_attribute_name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formatic
|
4
|
+
module Wrappers
|
5
|
+
# Determines whether an attributes is optional or not.
|
6
|
+
class Required
|
7
|
+
include Calls
|
8
|
+
|
9
|
+
option :manual_required
|
10
|
+
option :object
|
11
|
+
option :attribute_name
|
12
|
+
|
13
|
+
# Could also be made smarter, e.g. global configuration.
|
14
|
+
# See https://github.com/heartcombo/simple_form/blob/main/lib/simple_form/helpers/required.rb
|
15
|
+
def call
|
16
|
+
return true if manual_required == true
|
17
|
+
return false if manual_required == false
|
18
|
+
return false if validators.empty?
|
19
|
+
|
20
|
+
validators.any? { _1.kind == :presence }
|
21
|
+
end
|
22
|
+
|
23
|
+
# All applicable validatiors of this attribute.
|
24
|
+
def validators
|
25
|
+
@validators ||= ::Formatic::Wrappers::Validators.call(object:, attribute_name:)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|