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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +32 -0
  5. data/app/assets/javascript/formatic/components/date.ts +54 -0
  6. data/app/assets/javascript/formatic/components/select.ts +109 -0
  7. data/app/assets/javascript/formatic/components/stepper.ts +89 -0
  8. data/app/assets/javascript/formatic/components/textarea.ts +108 -0
  9. data/app/assets/javascript/formatic/components/toggle.ts +81 -0
  10. data/app/assets/javascript/formatic.js +350 -0
  11. data/app/assets/javascript/formatic.js.map +1 -0
  12. data/app/assets/stylesheets/formatic/components/checklist.sass +3 -0
  13. data/app/assets/stylesheets/formatic/components/date.sass +70 -0
  14. data/app/assets/stylesheets/formatic/components/select.sass +24 -0
  15. data/app/assets/stylesheets/formatic/components/stepper.sass +43 -0
  16. data/app/assets/stylesheets/formatic/components/string.sass +13 -0
  17. data/app/assets/stylesheets/formatic/components/textarea.sass +22 -0
  18. data/app/assets/stylesheets/formatic/components/toggle.sass +93 -0
  19. data/app/assets/stylesheets/formatic/components/wrapper.sass +51 -0
  20. data/app/assets/stylesheets/formatic/generics/flip.sass +3 -0
  21. data/app/assets/stylesheets/formatic/index.sass +16 -0
  22. data/app/assets/stylesheets/formatic/package.json +5 -0
  23. data/app/assets/stylesheets/formatic/scopes/form.sass +45 -0
  24. data/app/assets/stylesheets/formatic/settings/_colors.sass +13 -0
  25. data/app/assets/stylesheets/formatic/tools/terminal.sass +3 -0
  26. data/app/assets/stylesheets/formatic/utilities/container.sass +2 -0
  27. data/app/components/formatic/application_component.rb +39 -0
  28. data/app/components/formatic/base.rb +72 -0
  29. data/app/components/formatic/checklist.rb +65 -0
  30. data/app/components/formatic/date.rb +110 -0
  31. data/app/components/formatic/select.rb +49 -0
  32. data/app/components/formatic/stepper.rb +35 -0
  33. data/app/components/formatic/string.rb +57 -0
  34. data/app/components/formatic/textarea.rb +48 -0
  35. data/app/components/formatic/toggle.rb +57 -0
  36. data/app/components/formatic/wrapper.rb +122 -0
  37. data/config/locales/formatic.de.yml +5 -0
  38. data/config/locales/formatic.en.yml +5 -0
  39. data/lib/formatic/choices/countries.rb +14 -0
  40. data/lib/formatic/choices/keys.rb +27 -0
  41. data/lib/formatic/choices/options.rb +39 -0
  42. data/lib/formatic/choices/records.rb +47 -0
  43. data/lib/formatic/choices.rb +62 -0
  44. data/lib/formatic/css.rb +10 -0
  45. data/lib/formatic/engine.rb +40 -0
  46. data/lib/formatic/safe_join.rb +20 -0
  47. data/lib/formatic/templates/date.rb +76 -0
  48. data/lib/formatic/templates/select.rb +35 -0
  49. data/lib/formatic/templates/wrapper.rb +52 -0
  50. data/lib/formatic/version.rb +5 -0
  51. data/lib/formatic/wrappers/alternative_attribute_name.rb +18 -0
  52. data/lib/formatic/wrappers/error_messages.rb +39 -0
  53. data/lib/formatic/wrappers/required.rb +29 -0
  54. data/lib/formatic/wrappers/translate.rb +41 -0
  55. data/lib/formatic/wrappers/validators.rb +39 -0
  56. data/lib/formatic.rb +29 -0
  57. 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,5 @@
1
+ de:
2
+ formatic:
3
+
4
+ date:
5
+ blank: Leer
@@ -0,0 +1,5 @@
1
+ en:
2
+ formatic:
3
+
4
+ date:
5
+ blank: Blank
@@ -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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formatic
4
+ # Simple helper to unify Strings and Arrays of CSS class names.
5
+ class Css
6
+ def self.call(*input)
7
+ Array(input).flatten.compact.join(' ')
8
+ end
9
+ end
10
+ end
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formatic
4
+ VERSION = '0.1.0'
5
+ 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