proscenium 0.19.0.beta3-x86_64-linux → 0.19.0.beta5-x86_64-linux

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/proscenium/builder.rb +9 -13
  4. data/lib/proscenium/ext/proscenium +0 -0
  5. data/lib/proscenium/importer.rb +13 -13
  6. data/lib/proscenium/middleware/base.rb +0 -2
  7. data/lib/proscenium/middleware/engines.rb +5 -9
  8. data/lib/proscenium/middleware/esbuild.rb +13 -8
  9. data/lib/proscenium/middleware.rb +2 -4
  10. data/lib/proscenium/railtie.rb +6 -3
  11. data/lib/proscenium/react_componentable.rb +1 -1
  12. data/lib/proscenium/resolver.rb +3 -8
  13. data/lib/proscenium/side_load.rb +1 -1
  14. data/lib/proscenium/ui/flash/index.css +1 -0
  15. data/lib/proscenium/ui/flash/index.js +73 -0
  16. data/lib/proscenium/ui/flash.rb +15 -0
  17. data/lib/proscenium/ui/form/field_methods.rb +88 -0
  18. data/lib/proscenium/ui/form/fields/base.rb +188 -0
  19. data/lib/proscenium/ui/form/fields/checkbox/index.jsx +48 -0
  20. data/lib/proscenium/ui/form/fields/checkbox/index.module.css +9 -0
  21. data/lib/proscenium/ui/form/fields/checkbox/previews/basic.jsx +8 -0
  22. data/lib/proscenium/ui/form/fields/checkbox.rb +32 -0
  23. data/lib/proscenium/ui/form/fields/date.module.css +27 -0
  24. data/lib/proscenium/ui/form/fields/datetime.rb +15 -0
  25. data/lib/proscenium/ui/form/fields/hidden.rb +9 -0
  26. data/lib/proscenium/ui/form/fields/input/index.jsx +71 -0
  27. data/lib/proscenium/ui/form/fields/input/index.module.css +13 -0
  28. data/lib/proscenium/ui/form/fields/input/previews/basic.jsx +8 -0
  29. data/lib/proscenium/ui/form/fields/input.rb +14 -0
  30. data/lib/proscenium/ui/form/fields/radio_group.rb +173 -0
  31. data/lib/proscenium/ui/form/fields/radio_input/index.jsx +44 -0
  32. data/lib/proscenium/ui/form/fields/radio_input/index.module.css +13 -0
  33. data/lib/proscenium/ui/form/fields/radio_input/previews/basic.jsx +8 -0
  34. data/lib/proscenium/ui/form/fields/radio_input.rb +17 -0
  35. data/lib/proscenium/ui/form/fields/rich_textarea.css +23 -0
  36. data/lib/proscenium/ui/form/fields/rich_textarea.js +6 -0
  37. data/lib/proscenium/ui/form/fields/rich_textarea.rb +18 -0
  38. data/lib/proscenium/ui/form/fields/select.jsx +47 -0
  39. data/lib/proscenium/ui/form/fields/select.module.css +46 -0
  40. data/lib/proscenium/ui/form/fields/select.rb +300 -0
  41. data/lib/proscenium/ui/form/fields/tel.css +297 -0
  42. data/lib/proscenium/ui/form/fields/tel.js +83 -0
  43. data/lib/proscenium/ui/form/fields/tel.rb +54 -0
  44. data/lib/proscenium/ui/form/fields/textarea/index.jsx +50 -0
  45. data/lib/proscenium/ui/form/fields/textarea/index.module.css +13 -0
  46. data/lib/proscenium/ui/form/fields/textarea/previews/basic.jsx +8 -0
  47. data/lib/proscenium/ui/form/fields/textarea.rb +18 -0
  48. data/lib/proscenium/ui/form/translation.rb +71 -0
  49. data/lib/proscenium/ui/form.css +52 -0
  50. data/lib/proscenium/ui/form.rb +213 -0
  51. data/lib/proscenium/ui/props.css +7 -0
  52. data/lib/proscenium/ui/react-manager/index.jsx +1 -1
  53. data/lib/proscenium/ui/test.js +1 -1
  54. data/lib/proscenium/ui/ujs/index.js +1 -1
  55. data/lib/proscenium/ui.rb +3 -0
  56. data/lib/proscenium/utils.rb +33 -0
  57. data/lib/proscenium/version.rb +1 -1
  58. data/lib/proscenium/view_component.rb +0 -2
  59. data/lib/proscenium.rb +12 -2
  60. metadata +61 -10
  61. data/lib/proscenium/middleware/runtime.rb +0 -18
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Proscenium::UI
6
+ module Form::Fields
7
+ #
8
+ # Abstract class to provide basic rendering of an <input>. All field classes inherit this.
9
+ #
10
+ class Base < Component
11
+ attr_reader :attribute, :model, :form, :attributes
12
+
13
+ register_element :pui_field
14
+
15
+ # In most cases we want to use the main form component stylesheet. Override this method if
16
+ # you want to use a different stylesheet.
17
+ # def self.css_module_path
18
+ # source_path.join('../index.module.css')
19
+ # end
20
+
21
+ # @param attribute [Array]
22
+ # @param model [*]
23
+ # @param form [Proscenium::UI::Form]
24
+ # @param type [Symbol] input type, eg. 'text', 'select'
25
+ # @param error [ActiveModel::Error, String] error message for the attribute.
26
+ # @param attributes [Hash] HTML attributes to pass to the input.
27
+ def initialize(attribute, model, form, type: nil, error: nil, **attributes) # rubocop:disable Lint/MissingSuper,Metrics/ParameterLists
28
+ if attribute.count > 2
29
+ raise ArgumentError, 'attribute cannot be nested more than 2 levels deep'
30
+ end
31
+
32
+ @attribute = attribute
33
+ @model = model
34
+ @form = form
35
+ @field_type = type
36
+ @error = error
37
+ @attributes = attributes
38
+ end
39
+
40
+ private
41
+
42
+ # @return [String] The error message for the attribute.
43
+ def error_message
44
+ @error_message ||= case @error
45
+ when ActiveModel::Error
46
+ @error.message
47
+ when String
48
+ @error
49
+ else
50
+ if model.errors.include?(attribute.join('.'))
51
+ model.errors.where(attribute.join('.')).first&.message
52
+ elsif model.errors.include?(attribute.first)
53
+ model.errors.where(attribute.first).first&.message
54
+ end
55
+ end
56
+ end
57
+
58
+ def error?
59
+ error_message.present?
60
+ end
61
+
62
+ # The main wrapper for the field. This is where the label, input, and error message are
63
+ # rendered.
64
+ #
65
+ # @param tag_name: [Symbol] HTML tag name to use for the wrapper.
66
+ # @param ** [Hash] Additional HTML attributes to pass to the wrapper.
67
+ # @param [Proc] The block to render the field.
68
+ def field(tag_name = :pui_field, **rest, &)
69
+ classes = []
70
+ classes << rest.delete(:class) if rest.key?(:class)
71
+ classes << attributes.delete(:class) if attributes.key?(:class)
72
+
73
+ send(tag_name, class: classes, data: { field_error: error? }, **rest, &)
74
+ end
75
+
76
+ # Builds the template for the label, along with any error message for the attribute.
77
+ #
78
+ # By default, the translated attribute name will be used as the content for the label. You can
79
+ # overide this by providing the `:label` keyword argument in `@arguments`. Passing false as
80
+ # the value to `:label` will omit the label.
81
+ #
82
+ # If a block is given, it will be yielded with the label content, and after the label and
83
+ # error message.
84
+ def label(**kwargs, &block)
85
+ content = attributes.delete(:label)
86
+
87
+ super(**kwargs) do
88
+ captured = capture do
89
+ div do
90
+ span { content || translate_label } if content != false
91
+ error? && span(part: :error) { error_message }
92
+ end
93
+ end
94
+
95
+ if !block
96
+ yield_content_with_no_args { captured }
97
+ elsif block.arity == 1
98
+ yield captured
99
+ else
100
+ yield_content_with_no_args { captured }
101
+ yield
102
+ end
103
+ end
104
+ end
105
+
106
+ def hint(content = nil)
107
+ content ||= attributes.delete(:hint)
108
+
109
+ return if content == false
110
+
111
+ content ||= translate(:hints)
112
+ content.present? && div(part: :hint) { unsafe_raw content }
113
+ end
114
+
115
+ def field_type
116
+ @field_type ||= self.class.name.demodulize.underscore
117
+ end
118
+
119
+ def field_name(*names, multiple: false)
120
+ names.prepend attribute.last
121
+
122
+ if nested?
123
+ if nested_attributes_association?
124
+ names.prepend "#{attribute.first}_attributes"
125
+ else
126
+ names.prepend attribute.first
127
+ end
128
+ elsif names.count == 1 && names.first.is_a?(String)
129
+ return names.first
130
+ end
131
+
132
+ form.field_name(*names, multiple:)
133
+ end
134
+
135
+ def field_id(*)
136
+ @field_uid ||= SecureRandom.alphanumeric(10)
137
+ form.field_id(*attribute, @field_uid, *)
138
+ end
139
+
140
+ def translate(namespace, postfix: nil, default: '')
141
+ form.translate namespace, attribute, postfix:, default:
142
+ end
143
+
144
+ def translate_label(default: nil)
145
+ form.translate_label attribute, default:
146
+ end
147
+
148
+ def build_attributes(**attrs)
149
+ attributes.merge(attrs).tap do |x|
150
+ x[:value] ||= value.to_s
151
+ end
152
+ end
153
+
154
+ def value
155
+ attr = attribute.last
156
+ if actual_model.respond_to?(attr)
157
+ actual_model.public_send(attribute.last)
158
+ else
159
+ ''
160
+ end
161
+ end
162
+
163
+ # @return [Boolean] true if the attribute is nested, otherwise false.
164
+ def nested?
165
+ attribute.count > 1
166
+ end
167
+
168
+ def nested_attributes_association?
169
+ parent_model.respond_to?(:"#{attribute.first}_attributes=")
170
+ end
171
+
172
+ # @return the nested model if nested, otherwise nil.
173
+ def nested_model
174
+ @nested_model ||= nested? ? model.public_send(attribute.first) : nil
175
+ end
176
+
177
+ def actual_model
178
+ @actual_model ||= nested_model || model
179
+ end
180
+
181
+ alias parent_model model
182
+
183
+ def virtual_path
184
+ @virtual_path ||= Proscenium::Resolver.resolve self.class.source_path.sub_ext('.jsx').to_s
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,48 @@
1
+ import clsx from 'clsx'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import dsx from '/hue/lib/hue/utils/dsx'
5
+ import { useFormError } from '../../hooks'
6
+
7
+ import styles from './index.module.css'
8
+
9
+ const Component = ({ label, hint, className, errorAttrName, ...props }) => {
10
+ const [error, hasError] = useFormError(errorAttrName || props.name)
11
+
12
+ return (
13
+ <div className={clsx(styles.fieldWrapper, className)} {...dsx({ fieldError: hasError })}>
14
+ <label>
15
+ <input type="hidden" value="0" name={props.name} />
16
+ <input type="checkbox" value="1" {...props} />
17
+
18
+ <span>
19
+ {label ? <span>{label}</span> : null}
20
+ {hasError ? <span>{error}</span> : null}
21
+ </span>
22
+ </label>
23
+
24
+ {hint ? <div className={styles.hint}>{hint}</div> : null}
25
+ </div>
26
+ )
27
+ }
28
+
29
+ Component.displayName = 'Hue.Form.Fields.Checkbox'
30
+ Component.propTypes = {
31
+ name: PropTypes.string.isRequired,
32
+
33
+ label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.element]),
34
+
35
+ // Custom class name. This will be appended to the default class.
36
+ className: PropTypes.string,
37
+
38
+ // The name of the attribute to use for the error message. Default: 'props.name'.
39
+ errorAttrName: PropTypes.string,
40
+
41
+ id: PropTypes.string,
42
+ hint: PropTypes.string,
43
+ disabled: PropTypes.bool
44
+
45
+ // All remaining non-descript props will be forwarded to the <input> element.
46
+ }
47
+
48
+ export default Component
@@ -0,0 +1,9 @@
1
+ @layer hue-component {
2
+ .fieldWrapper {
3
+ @mixin checkbox from url('/hue/lib/hue/mixins/checkbox.mixin.css');
4
+ }
5
+
6
+ .hint {
7
+ @mixin fieldHint from url('/hue/lib/hue/mixins/field.mixin.css');
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ import Checkbox from '../'
2
+
3
+ const Component = () => {
4
+ return <Checkbox name="awesome" label="I am awesome?" />
5
+ }
6
+ Component.displayName = 'Hue.Form.Fields.Checkbox.Previews.Basic'
7
+
8
+ export default Component
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI::Form::Fields
4
+ # Renders a checkbox field similar to how Rails handles it.
5
+ #
6
+ # A predicate attribute name can be given:
7
+ #
8
+ # checkbox_field :active?
9
+ #
10
+ class Checkbox < Base
11
+ register_element :pui_checkbox
12
+
13
+ def view_template
14
+ checked = ActiveModel::Type::Boolean.new.cast(value.nil? ? false : value)
15
+
16
+ checked_value = attributes.delete(:checked_value) || '1'
17
+ unchecked_value = attributes.delete(:unchecked_value) || '0'
18
+
19
+ # TODO: use component
20
+ # render Proscenium::UI::Fields::Checkbox::Component.new field_name, checked:
21
+
22
+ field :pui_checkbox do
23
+ label do |content|
24
+ input(name: field_name, type: :hidden, value: unchecked_value, **attributes)
25
+ input(name: field_name, type: :checkbox, value: checked_value, checked:, **attributes)
26
+ yield_content_with_no_args { content }
27
+ end
28
+ hint
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ @layer hue-component {
2
+ .field_wrapper {
3
+ @mixin fieldWrapper from url('/hue/lib/hue/mixins/form.mixin.css');
4
+ }
5
+
6
+ .inputs {
7
+ display: flex;
8
+
9
+ & div {
10
+ padding-right: 10px;
11
+ }
12
+
13
+ & label > span {
14
+ color: var(--gray6);
15
+ font-size: var(--12px);
16
+ margin: 0 0 0.2em 0.2em;
17
+ }
18
+
19
+ > div {
20
+ width: 7em;
21
+ }
22
+ }
23
+
24
+ .hint {
25
+ @mixin fieldHint from url('/hue/lib/hue/mixins/field.mixin.css');
26
+ }
27
+ }
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI::Form::Fields
4
+ class Datetime < Input
5
+ def field_type
6
+ 'datetime-local'
7
+ end
8
+
9
+ private
10
+
11
+ def value
12
+ super&.strftime('%Y-%m-%dT%H:%M')
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI::Form::Fields
4
+ class Hidden < Base
5
+ def view_template
6
+ input(name: field_name, type: :hidden, **build_attributes)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,71 @@
1
+ import clsx from 'clsx'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import dsx from '/hue/lib/hue/utils/dsx'
5
+ import { useFormError } from '../../hooks'
6
+
7
+ import styles from './index.module.css'
8
+
9
+ const Input = ({
10
+ label,
11
+ hint,
12
+ className,
13
+ inputClassName,
14
+ errorAttrName,
15
+ inputRef,
16
+ children,
17
+ ...props
18
+ }) => {
19
+ const [error, hasError] = useFormError(errorAttrName || props.name)
20
+
21
+ return (
22
+ <div className={clsx(styles.fieldWrapper, className)} {...dsx({ fieldError: hasError })}>
23
+ <label>
24
+ <span>
25
+ {label ? <span>{label}</span> : null}
26
+ {hasError ? <span>{error}</span> : null}
27
+ </span>
28
+
29
+ {children || <input className={inputClassName || styles.input} {...props} ref={inputRef} />}
30
+ </label>
31
+
32
+ {hint ? <div className={styles.hint}>{hint}</div> : null}
33
+ </div>
34
+ )
35
+ }
36
+ Input.displayName = 'Hue.Form.Fields.Input'
37
+ Input.propTypes = {
38
+ name: PropTypes.string.isRequired,
39
+
40
+ label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.element]),
41
+
42
+ // Input `type` attribute. Default: 'text'.
43
+ type: PropTypes.string,
44
+
45
+ // Custom class name. This will be appended to the default class.
46
+ className: PropTypes.string,
47
+
48
+ // Custom class name for the actual input element. This will replace the default class.
49
+ inputClassName: PropTypes.string,
50
+
51
+ // The name of the attribute to use for the error message. Default: 'props.name'.
52
+ errorAttrName: PropTypes.string,
53
+
54
+ children: PropTypes.node,
55
+
56
+ inputRef: PropTypes.oneOfType([
57
+ PropTypes.func,
58
+ PropTypes.shape({ current: PropTypes.instanceOf(Element) })
59
+ ]),
60
+
61
+ id: PropTypes.string,
62
+ hint: PropTypes.string,
63
+ disabled: PropTypes.bool
64
+
65
+ // All remaining non-descript props will be forwarded to the <input> element.
66
+ }
67
+ Input.defaultProps = {
68
+ type: 'text'
69
+ }
70
+
71
+ export default Input
@@ -0,0 +1,13 @@
1
+ @layer hue-component {
2
+ .fieldWrapper {
3
+ @mixin fieldWrapper from url('/hue/lib/hue/mixins/form.mixin.css');
4
+ }
5
+
6
+ .input {
7
+ @mixin input from url('/hue/lib/hue/mixins/input.mixin.css');
8
+ }
9
+
10
+ .hint {
11
+ @mixin fieldHint from url('/hue/lib/hue/mixins/field.mixin.css');
12
+ }
13
+ }
@@ -0,0 +1,8 @@
1
+ import Input from '../'
2
+
3
+ const Component = () => {
4
+ return <Input name="name" label="Name" />
5
+ }
6
+ Component.displayName = 'Hue.Form.Fields.Input.Previews.Basic'
7
+
8
+ export default Component
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI::Form::Fields
4
+ class Input < Base
5
+ def view_template
6
+ field do
7
+ label do
8
+ input(name: field_name, type: field_type, **build_attributes)
9
+ end
10
+ hint
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI::Form::Fields
4
+ # Render a group of <radio> inputs for the given model attribute. It supports ActiveRecord
5
+ # associations and enums.
6
+ #
7
+ # ## Supported options
8
+ #
9
+ # - options [Array] a list of options where each will render a radio input. If this is given, the
10
+ # automatic detection of options will be disabled. A flat array of strings will be used for
11
+ # both the label and value. While an Array of nested two-level arrays will be used.
12
+ class RadioGroup < Base
13
+ register_element :pui_radio_group
14
+
15
+ def before_template
16
+ @options_from_attributes = attributes.delete(:options)
17
+
18
+ super
19
+ end
20
+
21
+ def view_template
22
+ field :pui_radio_group do
23
+ label
24
+
25
+ div part: :radio_group_inputs do
26
+ options.each do |opt|
27
+ form.radio_input(*attribute, name: field_name, value: opt[:value], label: opt[:label],
28
+ checked: opt[:checked], **attributes)
29
+ end
30
+ end
31
+
32
+ hint
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def field_name(*names, multiple: false)
39
+ names.prepend association_attribute? ? association_attribute : attribute.last
40
+
41
+ if nested?
42
+ if nested_attributes_association?
43
+ names.prepend "#{attribute.first}_attributes"
44
+ else
45
+ names.prepend attribute.first
46
+ end
47
+ elsif names.count == 1 && names.first.is_a?(String)
48
+ return names.first
49
+ end
50
+
51
+ form.field_name(*names, multiple:)
52
+ end
53
+
54
+ def options
55
+ if @options_from_attributes
56
+ @options_from_attributes.map do |x|
57
+ if x.is_a?(Array)
58
+ { value: x.first, label: x.last, checked: checked?(x.first) }
59
+ else
60
+ { value: x, label: x, checked: checked?(x) }
61
+ end
62
+ end
63
+ elsif enum_attribute?
64
+ fetch_enum_collection.map do |x|
65
+ {
66
+ value: x,
67
+ label: model_class.human_attribute_name("#{attribute.last}.#{x}"),
68
+ checked: checked?(x)
69
+ }
70
+ end
71
+ elsif association_attribute?
72
+ fetch_association_collection.map do |x|
73
+ {
74
+ value: x.id,
75
+ label: x.to_s,
76
+ checked: checked?(x)
77
+ }
78
+ end
79
+ end
80
+ end
81
+
82
+ def value
83
+ if actual_model.respond_to?(model_attribute)
84
+ actual_model.public_send(model_attribute)
85
+ else
86
+ ''
87
+ end
88
+ end
89
+
90
+ # Is the given `option` the current value (checked)?
91
+ def checked?(option)
92
+ if !option.is_a?(String) && !option.is_a?(Integer) && association_attribute?
93
+ reflection = association_reflection
94
+ key = if reflection.respond_to?(:options) && reflection.options[:primary_key]
95
+ reflection.options[:primary_key]
96
+ else
97
+ option.class.primary_key.to_s
98
+ end
99
+ option.attributes[key] == value
100
+ else
101
+ option.to_s == value.to_s
102
+ end
103
+ end
104
+
105
+ def model_attribute
106
+ @model_attribute ||= association_attribute? ? association_attribute : attribute.last
107
+ end
108
+
109
+ def enum_attribute?
110
+ model_class.defined_enums.key?(attribute.last.to_s)
111
+ end
112
+
113
+ def association_attribute?
114
+ association_reflection.present?
115
+ end
116
+
117
+ def association_attribute
118
+ @association_attribute ||= begin
119
+ reflection = association_reflection
120
+
121
+ case reflection.macro
122
+ when :belongs_to
123
+ (reflection.respond_to?(:options) && reflection.options[:foreign_key]&.to_sym) ||
124
+ :"#{reflection.name}_id"
125
+ else
126
+ # Force the association to be preloaded for performance.
127
+ if actual_model.respond_to?(attribute.last)
128
+ target = actual_model.send(attribute.last)
129
+ target.to_a if target.respond_to?(:to_a)
130
+ end
131
+
132
+ :"#{reflection.name.to_s.singularize}_ids"
133
+ end
134
+ end
135
+ end
136
+
137
+ def association_reflection
138
+ @association_reflection ||= model_class.try :reflect_on_association, attribute.last
139
+ end
140
+
141
+ def fetch_association_collection
142
+ relation = association_reflection.klass.all
143
+
144
+ # association_reflection.macro == :has_many
145
+
146
+ if association_reflection.respond_to?(:scope) && association_reflection.scope
147
+ relation = if association_reflection.scope.parameters.any?
148
+ association_reflection.klass.instance_exec(actual_model,
149
+ &association_reflection.scope)
150
+ else
151
+ association_reflection.klass.instance_exec(&association_reflection.scope)
152
+ end
153
+ else
154
+ order = association_reflection.options[:order]
155
+ conditions = association_reflection.options[:conditions]
156
+ conditions = actual_model.instance_exec(&conditions) if conditions.respond_to?(:call)
157
+
158
+ relation = relation.where(conditions) if relation.respond_to?(:where) && conditions.present?
159
+ relation = relation.order(order) if relation.respond_to?(:order)
160
+ end
161
+
162
+ relation
163
+ end
164
+
165
+ def fetch_enum_collection
166
+ actual_model.defined_enums[attribute.last.to_s].keys
167
+ end
168
+
169
+ def model_class
170
+ @model_class ||= actual_model.class
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,44 @@
1
+ import clsx from 'clsx'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import dsx from '/hue/lib/hue/utils/dsx'
5
+ import { useFormError } from '../../hooks'
6
+
7
+ import styles from './index.module.css'
8
+
9
+ const Component = ({ label, hint, className, errorAttrName, ...props }) => {
10
+ const [error, hasError] = useFormError(errorAttrName || props.name)
11
+
12
+ return (
13
+ <div className={clsx(styles.fieldWrapper, className)} {...dsx({ fieldError: hasError })}>
14
+ <label>
15
+ <input type="radio" {...props} />
16
+
17
+ <span>{label}</span>
18
+ </label>
19
+
20
+ {hasError ? <div className={styles.error}>{error}</div> : null}
21
+ {hint ? <div className={styles.hint}>{hint}</div> : null}
22
+ </div>
23
+ )
24
+ }
25
+
26
+ Component.displayName = 'Hue.Form.Fields.RadioInput'
27
+ Component.propTypes = {
28
+ name: PropTypes.string.isRequired,
29
+ label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.element]).isRequired,
30
+
31
+ // Custom class name. This will be appended to the default class.
32
+ className: PropTypes.string,
33
+
34
+ // The name of the attribute to use for the error message. Default: 'props.name'.
35
+ errorAttrName: PropTypes.string,
36
+
37
+ id: PropTypes.string,
38
+ hint: PropTypes.string,
39
+ disabled: PropTypes.bool
40
+
41
+ // All remaining non-descript props will be forwarded to the <input> element.
42
+ }
43
+
44
+ export default Component
@@ -0,0 +1,13 @@
1
+ @layer hue-component {
2
+ .fieldWrapper {
3
+ @mixin radio from url('/hue/lib/hue/mixins/radio.mixin.css');
4
+ }
5
+
6
+ .hint {
7
+ @mixin fieldHint from url('/hue/lib/hue/mixins/field.mixin.css');
8
+ }
9
+
10
+ .error {
11
+ @mixin fieldError from url('/hue/lib/hue/mixins/field.mixin.css');
12
+ }
13
+ }
@@ -0,0 +1,8 @@
1
+ import RadioInput from '../'
2
+
3
+ const Component = () => {
4
+ return <RadioInput name="awesome" label="I am awesome?" value="awesome" />
5
+ }
6
+ Component.displayName = 'Hue.Form.Fields.RadioInput.Previews.Basic'
7
+
8
+ export default Component