proscenium 0.19.0.beta4 → 0.19.0.beta5

Sign up to get free protection for your applications and to get access to all the features.
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