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,51 @@
1
+ @use "iglu/font-size"
2
+ @use 'iglu/spacing'
3
+ @use "formatic/settings/colors"
4
+ @use "iglu/responsive/settings/breakpoints"
5
+
6
+ .c-formatic-wrapper
7
+ display: grid
8
+ grid-row-gap: 0.2rem
9
+ grid-template-columns: auto
10
+ grid-template-areas: 'label' 'input' 'error' 'hint'
11
+ +font-size.default
12
+ +spacing.margin-bottom
13
+
14
+ &--hint-before-input
15
+ grid-template-areas: 'label' 'hint' 'input' 'error'
16
+
17
+ @each $column in label input error hint address
18
+ &__#{$column}
19
+ grid-area: #{$column}
20
+
21
+ &__input
22
+ // Allow for children of this div to autoscroll-x
23
+ min-width: 1px
24
+
25
+ &__error
26
+ color: colors.$paradise-pink
27
+
28
+ i
29
+ display: inline-block
30
+ height: 1em
31
+ width: 1em
32
+ animation: formatic-flip 4s infinite
33
+ background-size: 1em 1em
34
+ background-image: url("data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"1024\" height=\"1024\" viewBox=\"0 0 1024 1024\"><path d=\"M512 220.885l368.256 675.115h-736.512l368.256-675.115zM512 42.667l-512 938.667h1024l-512-938.667zM469.333 426.667h85.333v256h-85.333v-256zM512 842.667c-29.397 0-53.333-23.893-53.333-53.333s23.936-53.333 53.333-53.333 53.333 23.893 53.333 53.333-23.936 53.333-53.333 53.333z\" style=\"fill: #{colors.$paradise-pink}\"></path></svg>")
35
+
36
+ &__hint
37
+ opacity: 0.5
38
+ +spacing.margin-bottom--tiny
39
+ +font-size.smaller
40
+
41
+ &__prevent-submit-on-enter
42
+ position: absolute
43
+ margin-left: -9999px
44
+ visibility: hidden
45
+
46
+ @container (min-width: #{breakpoints.$large})
47
+ .c-formatic-wrapper
48
+ grid-template-columns: 1fr 1fr
49
+ grid-template-rows: min-content min-content min-content
50
+ grid-column-gap: 1rem
51
+ grid-template-areas: 'label input' 'hint input' 'error input'
@@ -0,0 +1,3 @@
1
+ @keyframes formatic-flip
2
+ 0%, 80%
3
+ transform: rotateY(360deg)
@@ -0,0 +1,16 @@
1
+ // ITCSS
2
+
3
+ @use "./generics/flip.sass"
4
+
5
+ @use "./components/wrapper.sass" // Wrapper comes first, rest alphabetically
6
+ @use "./components/checklist.sass"
7
+ @use "./components/date.sass"
8
+ @use "./components/select.sass"
9
+ @use "./components/stepper.sass"
10
+ @use "./components/string.sass"
11
+ @use "./components/textarea.sass"
12
+ @use "./components/toggle.sass"
13
+
14
+ @use "./utilities/container"
15
+
16
+ @use "./scopes/form"
@@ -0,0 +1,5 @@
1
+ {
2
+ "exports": {
3
+ "sass": "formatic/index.sass"
4
+ }
5
+ }
@@ -0,0 +1,45 @@
1
+ @use "iglu/font-size"
2
+ @use "formatic/settings/colors"
3
+
4
+ .s-formatic
5
+ input[type="email"],
6
+ input[type="number"],
7
+ input[type="password"],
8
+ input[type="search"],
9
+ input[type="tel"],
10
+ input[type="text"],
11
+ input[type="url"],
12
+ textarea,
13
+ select
14
+ -webkit-appearance: none
15
+ -moz-appearance: none
16
+ font-family: 'Roboto Condensed'
17
+ width: 100%
18
+ outline-offset: 0
19
+ outline-color: colors.$neon-yellow
20
+ border: 1px solid colors.$silver-gray
21
+ color: colors.$black
22
+ height: 2em
23
+ padding-top: 0.2em
24
+ padding-right: 0.4em
25
+ padding-left: 0.4em
26
+ box-shadow: 0 1px 2px rgba(10, 10, 10, 0.1) inset
27
+ box-sizing: border-box
28
+ +font-size.default
29
+
30
+ .formatic-wrapper.is-required &
31
+ border-left-color: colors.$paradise-pink
32
+ border-left-width: 2px
33
+
34
+ textarea
35
+ min-height: 7em
36
+
37
+ select
38
+ background-image: url("data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"32\" height=\"24\" viewBox=\"0 0 32 24\"><polygon points=\"0,0 32,0 16,24\" style=\"fill: rgb(51, 51, 51)\"></polygon></svg>")
39
+ background-position: right 0.5rem center
40
+ background-repeat: no-repeat
41
+ background-size: 9px 6px
42
+ box-shadow: none
43
+
44
+ ::placeholder
45
+ color: colors.$silver-gray
@@ -0,0 +1,13 @@
1
+ @use "sass:color"
2
+
3
+ $black: #222
4
+ $white: #fff
5
+
6
+ $paradise-pink: rgb(240, 30, 80)
7
+ $neon-yellow: #ffd400
8
+ $jade-green: rgb(0, 190, 130)
9
+
10
+ $granite-gray: rgb(70, 70, 70)
11
+ $silver-gray: color.adjust($granite-gray, $lightness: 50%)
12
+
13
+ // $neon-red: #ec5840
@@ -0,0 +1,3 @@
1
+ =terminal
2
+ &
3
+ font-family: Inconsolata, "Roboto Mono", "Source Code Pro", "SF Mono", Monaco, "Fira Mono", "Droid Sans Mono", monospace !important
@@ -0,0 +1,2 @@
1
+ .u-formatic-container
2
+ container-type: inline-size
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formatic
4
+ # A component that every other component inherits from in this gem.
5
+ # It adds convenience methods for initialization of a component.
6
+ class ApplicationComponent < ViewComponent::Base
7
+ extend Dry::Initializer
8
+
9
+ # By default, Dry::Initializer doesn't complain about invalid arguments.
10
+ # We want it to raise an error.
11
+ def initialize(...)
12
+ __check_for_unknown_options(...)
13
+ super
14
+ end
15
+
16
+ private
17
+
18
+ def __check_for_unknown_options(*args, **kwargs)
19
+ return if __defined_options.empty?
20
+
21
+ # Checking params
22
+ opts = args.drop(__defined_params.length).first || kwargs
23
+ raise ArgumentError, "Unexpected argument #{opts}" unless opts.is_a? Hash
24
+
25
+ # Checking options
26
+ unknown_options = opts.keys - __defined_options
27
+ message = "Key(s) #{unknown_options} not found in #{__defined_options} of #{self.class}"
28
+ raise KeyError, message if unknown_options.any?
29
+ end
30
+
31
+ def __defined_options
32
+ self.class.dry_initializer.options.map(&:source)
33
+ end
34
+
35
+ def __defined_params
36
+ self.class.dry_initializer.params.map(&:source)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formatic
4
+ # All inputs inherit from this class.
5
+ class Base < ApplicationComponent
6
+ # Rails form builder. Usually with a model as `f.object`.
7
+ option :f
8
+
9
+ # The method that is called on the form object.
10
+ #
11
+ # This is uncontroversial, both Rails and SimpleForm have this.
12
+ # Rails: `f.text_field(:title)`
13
+ # SimpleForm: `f.input(:title)`
14
+ option :attribute_name, type: proc(&:to_sym)
15
+
16
+ # If passed in, used as the `<input value="...">`
17
+ # If not passed in, it is derived from the form object.
18
+ option :value, as: :manual_value, default: -> { :_fetch_from_record }
19
+
20
+ # CSS class(es) applied to the <input> element
21
+ # and the wrapper <div> respectively.
22
+ option :class, as: :manual_class, optional: true
23
+ option :wrapper_class, optional: true
24
+
25
+ # For inputs that support `<input autofocus=...>`
26
+ option :autofocus, default: -> { false }
27
+
28
+ # Some inputs (such as checkboxes and textfields)
29
+ # can be submitted continously by submitting their <form>
30
+ # via javascript.
31
+ option :async_submit, default: -> { false }
32
+
33
+ # See `Formatic::Wrapper`
34
+ option :label, default: -> { true }
35
+ option :label_for_id, optional: true
36
+ option :readonly, as: :readonly, default: -> { false }
37
+ option :required, optional: true
38
+ option :prevent_submit_on_enter, default: -> { false }
39
+
40
+ def wrapper
41
+ @wrapper ||= ::Formatic::Wrapper.new(
42
+ f:,
43
+ attribute_name:,
44
+ label:,
45
+ required:,
46
+ prevent_submit_on_enter:,
47
+ label_for_id:,
48
+ class: wrapper_class
49
+ )
50
+ end
51
+
52
+ def value
53
+ return manual_value if manual_value != :_fetch_from_record
54
+
55
+ f.object.public_send(attribute_name) if f.object.respond_to?(attribute_name)
56
+ end
57
+
58
+ # ---------------------------
59
+ # ActiveModel and Rails slugs
60
+ # ---------------------------
61
+
62
+ # Name of the URL param for this record.
63
+ def param_key
64
+ f.object.model_name.param_key
65
+ end
66
+
67
+ # # Name of the URL param for this input.
68
+ def input_name
69
+ "#{param_key}[#{attribute_name}]"
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formatic
4
+ # Multiple checkboxes for an Array of values in one attribute.
5
+ class Checklist < ::Formatic::Base
6
+ # Alternative 1:
7
+ # Raw options that are passed on to the Rails collection_check_boxes.check_box.
8
+ option :options, optional: true
9
+
10
+ # Alternative 2:
11
+ # ActiveRecord records used to populate the checkbox list.
12
+ option :records, optional: true
13
+
14
+ # Alternative 3:
15
+ # Keys to lookup in i18n translations.
16
+ option :keys, optional: true
17
+
18
+ option :include_current, optional: true
19
+
20
+ renders_many :toggles, ::Formatic::Toggle
21
+
22
+ # This is highjacking the CSS definitions of another component, `Formatic::Toggle`.
23
+ # I'm not terribly pleased by this, but I think it's a good work-around.
24
+ erb_template <<~ERB
25
+ <%= render wrapper do |wrap| %>
26
+
27
+ <% wrap.with_input do %>
28
+ <div class="c-formatic-checklist s-formatic">
29
+
30
+ <% f.collection_check_boxes(attribute_name, choices, :last, :first) do |builder| %>
31
+
32
+ <%= content_tag :div,
33
+ builder.label { builder.check_box(class: manual_class) + content_tag(:i) + content_tag(:span, split_and_wrap(builder.object.first)) },
34
+ class: 'c-formatic-toggle' %>
35
+
36
+ <% end %>
37
+
38
+ </div>
39
+ <% end %>
40
+ <% end %>
41
+ ERB
42
+
43
+ def choices
44
+ ::Formatic::Choices.call(
45
+ f:,
46
+ attribute_name:,
47
+ options:,
48
+ records:,
49
+ keys:,
50
+ include_current:,
51
+ include_blank: false
52
+ )
53
+ end
54
+
55
+ def split_and_wrap(string)
56
+ parts = string.split('   ')
57
+ return parts.first if parts.size == 1
58
+
59
+ main_part = parts[0..-2].join('   ')
60
+ last_part = parts.last
61
+
62
+ ::Formatic::SafeJoin.call(main_part, '<br/>'.html_safe, content_tag(:small, last_part))
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'formatic/templates/date'
4
+
5
+ module Formatic
6
+ # Date/calendar
7
+ class Date < ::Formatic::Base
8
+ # Represents one element in the calendar.
9
+ class Day
10
+ extend Dry::Initializer
11
+
12
+ option :date
13
+
14
+ def classes
15
+ [
16
+ ('is-today' if date.today?),
17
+ ('is-saturday' if date.saturday?),
18
+ ('is-sunday' if date.sunday?),
19
+ ('is-holiday' if holiday?)
20
+ ].join(' ')
21
+ end
22
+
23
+ private
24
+
25
+ def holiday?
26
+ return false if date.saturday? || date.sunday?
27
+
28
+ ::Holidays.on(date, :de_nw).present?
29
+ end
30
+ end
31
+
32
+ option :discard_day, optional: true
33
+
34
+ erb_template(::Formatic::Templates::Date.call)
35
+
36
+ def css_classes
37
+ %i[c-formatic-date__input]
38
+ end
39
+
40
+ def options_for_day
41
+ options_for_select collection_for_day, day_value
42
+ end
43
+
44
+ def options_for_month
45
+ options_for_select collection_for_month, f.object.public_send(attribute_name)&.month
46
+ end
47
+
48
+ def options_for_year
49
+ options_for_select collection_for_year, f.object.public_send(attribute_name)&.year
50
+ end
51
+
52
+ def day_attribute_name
53
+ "#{f.object.model_name.param_key}[#{attribute_name}(3i)]"
54
+ end
55
+
56
+ def month_attribute_name
57
+ "#{f.object.model_name.param_key}[#{attribute_name}(2i)]"
58
+ end
59
+
60
+ def year_attribute_name
61
+ "#{f.object.model_name.param_key}[#{attribute_name}(1i)]"
62
+ end
63
+
64
+ def day_value
65
+ f.object.public_send(attribute_name)&.day
66
+ end
67
+
68
+ def day_input_id
69
+ "#{f.object.model_name.param_key}_#{attribute_name}_3i"
70
+ end
71
+
72
+ def month_input_id
73
+ "#{f.object.model_name.param_key}_#{attribute_name}_2i"
74
+ end
75
+
76
+ def year_input_id
77
+ "#{f.object.model_name.param_key}_#{attribute_name}_1i"
78
+ end
79
+
80
+ def calendar(now: Time.current)
81
+ from = 5.days.ago.to_date
82
+ till = now.beginning_of_month.advance(months: 2).end_of_month.to_date
83
+
84
+ (from..till).map do |date|
85
+ ::Formatic::Date::Day.new(date:)
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def collection_for_day
92
+ return (1..31) if wrapper.required?
93
+
94
+ [nil] + (1..31).to_a
95
+ end
96
+
97
+ def collection_for_month
98
+ result = (1..12).map { [l(::Date.new(1, _1), format: '%B  %-m'), _1] }
99
+ result.prepend([nil, nil]) if wrapper.optional?
100
+ result
101
+ end
102
+
103
+ def collection_for_year
104
+ result = (30.years.ago.year..10.years.from_now.year)
105
+ return result if wrapper.required?
106
+
107
+ result.to_a.prepend nil
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'formatic/templates/select'
4
+
5
+ module Formatic
6
+ # Dropdown box
7
+ class Select < ::Formatic::Base
8
+ # Alternative 1:
9
+ # Raw options that are passed on to the Rails select_box_tag.
10
+ option :options, optional: true
11
+
12
+ # Alternative 2:
13
+ # ActiveRecord records used to populate the select box.
14
+ option :records, optional: true
15
+
16
+ # Alternative 3:
17
+ # Keys to lookup in i18n translations.
18
+ option :keys, optional: true
19
+
20
+ # Whether or not to show an empty (nil) option.
21
+ option :include_blank, default: -> { :guess }
22
+
23
+ option :include_current, optional: true
24
+
25
+ erb_template(::Formatic::Templates::Select.call)
26
+
27
+ def choices
28
+ ::Formatic::Choices.call(
29
+ f:,
30
+ attribute_name:,
31
+ options:,
32
+ records:,
33
+ keys:,
34
+ include_current:,
35
+ include_blank: include_blank?
36
+ )
37
+ end
38
+
39
+ def current_choice_name
40
+ choices.detect { _1.last == value }&.first
41
+ end
42
+
43
+ def include_blank?
44
+ return wrapper.optional? if @include_blank == :guess
45
+
46
+ !!@include_blank
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formatic
4
+ # Stepper input for integer values.
5
+ class Stepper < ::Formatic::Base
6
+ option :minimum, default: -> { 0 }
7
+
8
+ erb_template <<~ERB
9
+ <%= render wrapper do |wrap| %>
10
+
11
+ <% wrap.with_input do %>
12
+ <div class="c-formatic-stepper js-formatic-stepper">
13
+
14
+ <%= link_to '#', class: 'c-formatic-stepper__step js-formatic-stepper__decrement' do %>
15
+ &minus;
16
+ <% end %>
17
+ <%= text_field_tag input_name,
18
+ value.to_i,
19
+ { \
20
+ min: minimum,
21
+ inputmode: :numeric,
22
+ pattern: '-?[0-9]*',
23
+ class: 'c-formatic-stepper__number js-formatic-stepper__number',
24
+ placeholder: wrapper.placeholder,
25
+ } %>
26
+ <%= link_to '#', class: 'c-formatic-stepper__step js-formatic-stepper__increment' do %>
27
+ &plus;
28
+ <% end %>
29
+ </div>
30
+
31
+ <% end %>
32
+ <% end %>
33
+ ERB
34
+ end
35
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formatic
4
+ # Text input for one-liners.
5
+ class String < ::Formatic::Base
6
+ option :terminal, default: -> { false }
7
+
8
+ erb_template <<~ERB
9
+ <%= render wrapper do |wrap| -%>
10
+
11
+ <% wrap.with_input do -%>
12
+ <div class="c-formatic-string s-formatic">
13
+
14
+ <% if readonly -%>
15
+ <div class="s-markdown">
16
+ <p>
17
+ <% if terminal? -%>
18
+ <tt><%= value -%></tt>
19
+ <% else -%>
20
+ <%= value -%>
21
+ <% end -%>
22
+ </p>
23
+ </div>
24
+ <% else -%>
25
+ <%= f.text_field(attribute_name, **input_options) -%>
26
+ <% end -%>
27
+
28
+ </div>
29
+ <% end -%>
30
+ <% end -%>
31
+ ERB
32
+
33
+ def input_options
34
+ result = {
35
+ placeholder: wrapper.placeholder,
36
+ autofocus:,
37
+ class: css_classes
38
+ }
39
+
40
+ (result[:value] = value) if value
41
+
42
+ result
43
+ end
44
+
45
+ def css_classes
46
+ classes = %i[c-formatic-string__input]
47
+ classes.push(:'is-terminal') if terminal?
48
+ classes
49
+ end
50
+
51
+ private
52
+
53
+ def terminal?
54
+ !!@terminal
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formatic
4
+ # Text input for multi-line text.
5
+ class Textarea < ::Formatic::Base
6
+ renders_one :footer
7
+
8
+ erb_template <<~ERB
9
+ <%= render wrapper do |wrap| -%>
10
+
11
+ <% wrap.with_input do -%>
12
+ <div class="c-formatic-textarea s-formatic">
13
+
14
+ <% if readonly -%>
15
+ <div class="s-markdown">
16
+ <p>
17
+ <%= value -%>
18
+ </p>
19
+ </div>
20
+ <% else -%>
21
+ <%= f.text_area(attribute_name, **input_options) -%>
22
+ <% end -%>
23
+
24
+ </div>
25
+ <% end -%>
26
+ <% end -%>
27
+ ERB
28
+
29
+ def input_options
30
+ result = {
31
+ placeholder: wrapper.placeholder,
32
+ data: { '1p-ignore' => true },
33
+ autofocus:,
34
+ class: css_classes
35
+ }
36
+
37
+ (result[:value] = value) if value
38
+
39
+ result
40
+ end
41
+
42
+ def css_classes
43
+ classes = %i[c-formatic-textarea__input js-formatic-textarea]
44
+ classes.push(:'is-autosubmit') if async_submit
45
+ classes
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formatic
4
+ # Stylish checkbox for a boolean attribute.
5
+ class Toggle < ::Formatic::Base
6
+ erb_template <<~ERB
7
+ <%= render wrapper do |wrap| %>
8
+
9
+ <% wrap.with_input do %>
10
+ <div class="c-formatic-toggle s-formatic <%= 'js-formatic-toggle' if async_submit %>">
11
+
12
+ <% if readonly %>
13
+ <div class="s-markdown">
14
+ <p>
15
+ <%= value %>
16
+ </p>
17
+ </div>
18
+ <% else %>
19
+ <%= f.label attribute_name, nil, { for: dom_id } do |builder| %>
20
+ <%
21
+ f.check_box(attribute_name, { id: dom_id, class: css_classes }) +
22
+ content_tag(:i) +
23
+ content_tag(:div, human_attribute_name, class: 'c-formatic-toggle__label-caption-dummy') +
24
+ content_tag(:div, wrap.toggle_on, class: 'is-active') +
25
+ content_tag(:div, wrap.toggle_off, class: 'is-inactive')
26
+ %>
27
+ <% end %>
28
+ <% end %>
29
+
30
+ </div>
31
+ <% end %>
32
+ <% end %>
33
+ ERB
34
+
35
+ def css_classes
36
+ ::Formatic::Css.call('c-formatic-toggle__input', manual_class)
37
+ end
38
+
39
+ # So that the wrapper <label> references our custom checkbox.
40
+ def label_for_id
41
+ dom_id
42
+ end
43
+
44
+ def human_attribute_name
45
+ return unless f.object
46
+
47
+ f.object.class.human_attribute_name(attribute_name)
48
+ end
49
+
50
+ # There can be multiple checkboxes with the same attribute name on the page.
51
+ # To couple a <label> to its checkbox, make the checkbox ID unequivocal.
52
+ def dom_id
53
+ # For predictability in tests, maybe use something like `Time.now.nsec`?
54
+ @dom_id ||= SecureRandom.hex(4)
55
+ end
56
+ end
57
+ end