view_component-props 0.0.0 → 0.0.1
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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +153 -6
- data/lib/view_component_props/casters/base.rb +60 -0
- data/lib/view_component_props/casters.rb +36 -0
- data/lib/view_component_props/configuration.rb +39 -0
- data/lib/view_component_props/definable.rb +65 -0
- data/lib/view_component_props/definition.rb +116 -0
- data/lib/view_component_props/errors.rb +13 -0
- data/lib/view_component_props/railtie.rb +12 -0
- data/lib/view_component_props/version.rb +5 -0
- data/lib/view_component_props.rb +35 -0
- metadata +34 -7
- data/lib/view_component/props/configuration.rb +0 -8
- data/lib/view_component/props/errors.rb +0 -7
- data/lib/view_component/props/version.rb +0 -7
- data/lib/view_component/props.rb +0 -31
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f80ece3769d7472183a42e86a7fc2a37264f5c59b8131d25f9294d6d64f49fae
|
|
4
|
+
data.tar.gz: e95f39dfd49633c8ed3a32ae61c09ceba1cd06c48dccda877a54017dc45c440d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 057e8e1859b94a01f253211516527f3593d840dfdbb50db30832c24b68965bcf966cf2a0fa35b27443fb54a40d25c1ff90f5a8dd7dc6b990d5fa184981ef90bf
|
|
7
|
+
data.tar.gz: f01e9685447e35ae07715326302abf184e626dc3e39e1ecc151a5ab4282d54eecc6c331cb7265bdd95eb45e9ab34573e1f863b3f6d151cb259a754da019071b7
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [X.X.X] - YYYY-MM-DD
|
|
9
9
|
|
|
10
|
+
## [0.0.1] - 2026-05-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `prop` DSL with `default`, `fallback`, `required`, `cast`, `enum`, `validate`, and `description` options
|
|
15
|
+
- Resolved `props` (frozen indifferent hash) and `raw_props` on each instance; `prop_definitions` on the component class for introspection
|
|
16
|
+
- Built-in casters: `:integer`, `:float`, `:string`, `:symbol`, `:boolean`, `:array`, `:hash`, `:decimal`, `:date`, `:datetime`
|
|
17
|
+
- `ViewComponentProps.configure` for global settings and `register_caster` for custom or overriding casters
|
|
18
|
+
- Auto-install into `ViewComponent::Base` (Rails Railtie after initializers, or on require outside Rails); `#initialize` accepts a props hash and invokes `#after_initialize` after resolution
|
|
19
|
+
- Global `reject_undefined_props` configuration and per-class `reject_undefined_props!` / `permit_undefined_props!`
|
|
20
|
+
- `ViewComponentProps.install!` for patching additional base classes outside `ViewComponent::Base`
|
|
21
|
+
- Typed errors under `ViewComponentProps::Error` (`UnknownOptionError`, `UnknownCastError`, `CastError`, `RequiredPropError`, `InvalidEnumValueError`, `ValidationFailedError`, `UnknownPropsError`)
|
|
22
|
+
|
|
10
23
|
## [0.0.0] - 2026-05-20
|
|
11
24
|
|
|
12
25
|
- Initial repository scaffolding and gem publication. No functionality yet.
|
data/README.md
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ViewComponentProps
|
|
2
2
|
|
|
3
|
-
A [ViewComponent](https://viewcomponent.org) extension
|
|
3
|
+
A [ViewComponent](https://viewcomponent.org) extension that adds a `prop` DSL to components: defaults, fallbacks, required props, casting, enum validation, custom validators, and a pluggable caster registry.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
> This gem is in early development. The public API is not yet stable and there is no usable functionality in this release. Documentation and examples will land alongside the first feature release.
|
|
5
|
+
In Rails apps, a Railtie patches `ViewComponent::Base` during boot (enabled by default). Outside Rails, the gem patches on require. Inherit as usual and pass a props hash to `new`.
|
|
7
6
|
|
|
8
7
|
## Requirements
|
|
9
8
|
|
|
10
9
|
- Ruby >= 3.0
|
|
11
|
-
- ViewComponent >= 3.0
|
|
10
|
+
- [ViewComponent](https://viewcomponent.org) >= 3.0, < 5.0
|
|
11
|
+
- ActiveSupport >= 6.0, < 9.0
|
|
12
|
+
- ActiveModel >= 6.0, < 9.0
|
|
13
|
+
|
|
14
|
+
Rails is supported via a Railtie but not required; the gem auto-installs into `ViewComponent::Base` on require when Rails is not loaded.
|
|
12
15
|
|
|
13
16
|
## Installation
|
|
14
17
|
|
|
@@ -26,7 +29,151 @@ bundle install
|
|
|
26
29
|
|
|
27
30
|
## Usage
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
```ruby
|
|
33
|
+
class ButtonComponent < ViewComponent::Base
|
|
34
|
+
CLASS = %w[
|
|
35
|
+
px-9
|
|
36
|
+
py-3
|
|
37
|
+
text-lg
|
|
38
|
+
font-medium
|
|
39
|
+
tracking-wider
|
|
40
|
+
ring-0
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
prop :label, required: true
|
|
44
|
+
prop :shape, cast: :symbol, enum: %i[pill rounded rectangle], default: :pill
|
|
45
|
+
prop :disabled, cast: :boolean, default: false
|
|
46
|
+
|
|
47
|
+
def before_render
|
|
48
|
+
@class = cn(CLASS, {
|
|
49
|
+
"rounded-full" => @props[:shape] == :pill,
|
|
50
|
+
"rounded-xl" => @props[:shape] == :rounded,
|
|
51
|
+
"rounded-none" => @props[:shape] == :rectangle,
|
|
52
|
+
}, @props[:class])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def call
|
|
56
|
+
tag.button(@props[:label], class: @class, disabled: @props[:disabled])
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
render ButtonComponent.new(label: "Save", shape: :rounded)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Resolved props are available as `props` (or `@props`), a frozen hash you can read with either string or symbol keys. The original input, before any casting or defaults are applied, is available as `raw_props` (or `@raw_props`).
|
|
64
|
+
|
|
65
|
+
Once props are resolved, `#after_initialize` runs, so you can override it for any setup that depends on `props`.
|
|
66
|
+
|
|
67
|
+
### Options
|
|
68
|
+
|
|
69
|
+
Each `prop` accepts:
|
|
70
|
+
|
|
71
|
+
| Option | Purpose |
|
|
72
|
+
| -------------- | ---------------------------------------------------------------- |
|
|
73
|
+
| `default:` | Value (or callable) used when the key is missing from the input. |
|
|
74
|
+
| `fallback:` | Value (or callable) used when the resolved value is `nil`. |
|
|
75
|
+
| `required:` | Raises `RequiredPropError` when the resolved value is `nil`. |
|
|
76
|
+
| `cast:` | Coerces the value. A built-in caster name or any callable. |
|
|
77
|
+
| `enum:` | Restricts the value to a list of allowed values. |
|
|
78
|
+
| `validate:` | Callable that must return truthy for the value to be accepted. |
|
|
79
|
+
| `description:` | Free-form string for documentation and tooling. |
|
|
80
|
+
|
|
81
|
+
Defaults and fallbacks can also be callables, evaluated in the context of the component instance. Use `raw_props` to read other props from the constructor input:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class AvatarComponent < ViewComponent::Base
|
|
85
|
+
prop :user, required: true
|
|
86
|
+
prop :alt, default: -> { "#{raw_props[:user].name}'s avatar" }
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Casters
|
|
91
|
+
|
|
92
|
+
Built-in casters: `:integer`, `:float`, `:string`, `:symbol`, `:boolean`, `:array`, `:hash`, `:decimal`, `:date`, `:datetime`.
|
|
93
|
+
|
|
94
|
+
A `nil` value is never cast and stays `nil`. Use `default:` or `fallback:` when you need a value for a missing or `nil` prop.
|
|
95
|
+
|
|
96
|
+
Register custom casters in the configuration block:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
ViewComponentProps.configure do |config|
|
|
100
|
+
config.register_caster(:slug) do |value|
|
|
101
|
+
value.to_s.parameterize
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class HeadingComponent < ViewComponent::Base
|
|
106
|
+
prop :anchor, cast: :slug
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Built-in casters can be overridden the same way by registering the same key again in `configure`.
|
|
111
|
+
|
|
112
|
+
Or pass a lambda inline:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
prop :code, cast: ->(value) { value.to_s.upcase }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Strict props
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
class FormFieldComponent < ViewComponent::Base
|
|
122
|
+
reject_undefined_props!
|
|
123
|
+
|
|
124
|
+
prop :name, required: true
|
|
125
|
+
prop :label
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
FormFieldComponent.new(name: "email", typo: true)
|
|
129
|
+
# => raises ViewComponentProps::UnknownPropsError
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Custom base classes
|
|
133
|
+
|
|
134
|
+
`ViewComponentProps.install!(MyComponentBase)` applies the same patch to another class (idempotent). Useful if your app uses a shared component superclass that does **not** inherit from `ViewComponent::Base`.
|
|
135
|
+
|
|
136
|
+
`install!` no-ops when the target already includes `Definable` through any ancestor, so calling it on a subclass of `ViewComponent::Base` does nothing (the patch is already inherited). You only need it for base classes outside the `ViewComponent::Base` hierarchy.
|
|
137
|
+
|
|
138
|
+
## Configuration
|
|
139
|
+
|
|
140
|
+
### Rails
|
|
141
|
+
|
|
142
|
+
Disable auto-install in `config/application.rb`:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
config.view_component_props.auto_include = false
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
When enabled (the default), `ViewComponent::Base` is patched after initializers load so `config/initializers` can configure the gem first.
|
|
149
|
+
|
|
150
|
+
### Initializer
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
ViewComponentProps.configure do |config|
|
|
154
|
+
config.reject_undefined_props = true
|
|
155
|
+
|
|
156
|
+
config.register_caster(:slug) do |value|
|
|
157
|
+
value.to_s.parameterize
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
When `reject_undefined_props` is `true`, every component rejects unknown prop keys by default. Override per class with `reject_undefined_props!` or `permit_undefined_props!`.
|
|
163
|
+
|
|
164
|
+
### Non-Rails
|
|
165
|
+
|
|
166
|
+
Auto-install runs on require when `configuration.auto_include` is `true` (the default). Because it is read at require time, you must configure it **before** requiring the gem for it to have any effect:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
require "view_component_props/configuration"
|
|
170
|
+
ViewComponentProps.configure { |config| config.auto_include = false }
|
|
171
|
+
|
|
172
|
+
require "view_component_props"
|
|
173
|
+
ViewComponentProps.install!
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Configuring `auto_include` _after_ `require "view_component_props"` cannot reverse the install that already happened. Note this `configuration.auto_include` switch is consulted only outside Rails; in Rails the equivalent switch is `config.view_component_props.auto_include`, handled by the Railtie.
|
|
30
177
|
|
|
31
178
|
## Development
|
|
32
179
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponentProps
|
|
4
|
+
module Casters
|
|
5
|
+
class Base
|
|
6
|
+
BOOLEAN_TYPE = ActiveModel::Type::Boolean.new
|
|
7
|
+
|
|
8
|
+
class_attribute :base_casters, instance_accessor: false, default: HashWithIndifferentAccess.new
|
|
9
|
+
|
|
10
|
+
def self.register_caster(key, &block)
|
|
11
|
+
Casters.registry[key] = block
|
|
12
|
+
self.base_casters = base_casters.merge(key => block)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
register_caster(:integer) do |value|
|
|
16
|
+
Integer(value)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
register_caster(:float) do |value|
|
|
20
|
+
Float(value)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
register_caster(:string) do |value|
|
|
24
|
+
value.to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
register_caster(:symbol) do |value|
|
|
28
|
+
value.to_sym
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
register_caster(:boolean) do |value|
|
|
32
|
+
BOOLEAN_TYPE.cast(value)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
register_caster(:array) do |value|
|
|
36
|
+
Array(value)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
register_caster(:hash) do |value|
|
|
40
|
+
value.is_a?(Hash) ? value : value.to_h
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
register_caster(:decimal) do |value|
|
|
44
|
+
value.is_a?(BigDecimal) ? value : BigDecimal(value.to_s)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
register_caster(:date) do |value|
|
|
48
|
+
value.is_a?(Date) ? value : Date.parse(value.to_s)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
register_caster(:datetime) do |value|
|
|
52
|
+
if value.is_a?(Time) || value.is_a?(DateTime)
|
|
53
|
+
value
|
|
54
|
+
else
|
|
55
|
+
Time.zone ? Time.zone.parse(value.to_s) : Time.parse(value.to_s)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
4
|
+
require "active_support/core_ext/module/attribute_accessors"
|
|
5
|
+
require "bigdecimal"
|
|
6
|
+
require "date"
|
|
7
|
+
require "time"
|
|
8
|
+
require "active_model"
|
|
9
|
+
require "active_model/type"
|
|
10
|
+
require "active_model/type/boolean"
|
|
11
|
+
|
|
12
|
+
require_relative "configuration"
|
|
13
|
+
|
|
14
|
+
module ViewComponentProps
|
|
15
|
+
module Casters
|
|
16
|
+
mattr_accessor :registry, default: HashWithIndifferentAccess.new
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def fetch(key)
|
|
20
|
+
registry.fetch(key)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def known?(key)
|
|
24
|
+
registry.key?(key)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reset!
|
|
28
|
+
self.registry = Base.base_casters.merge(ViewComponentProps.configuration.custom_casters)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
require_relative "casters/base"
|
|
35
|
+
|
|
36
|
+
ViewComponentProps::Casters.reset!
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
4
|
+
|
|
5
|
+
module ViewComponentProps
|
|
6
|
+
class << self
|
|
7
|
+
def configuration
|
|
8
|
+
@configuration ||= Configuration.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def configure
|
|
12
|
+
yield(configuration)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def reset_configuration!
|
|
16
|
+
@configuration = Configuration.new
|
|
17
|
+
Casters.reset! if defined?(Casters)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Configuration
|
|
22
|
+
attr_accessor :auto_include
|
|
23
|
+
attr_accessor :reject_undefined_props
|
|
24
|
+
attr_reader :custom_casters
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
@auto_include = true
|
|
28
|
+
@reject_undefined_props = false
|
|
29
|
+
@custom_casters = HashWithIndifferentAccess.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def register_caster(key, &block)
|
|
33
|
+
raise ArgumentError, "register_caster requires a block" unless block
|
|
34
|
+
|
|
35
|
+
custom_casters[key] = block
|
|
36
|
+
Casters.registry[key] = block if defined?(Casters)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module ViewComponentProps
|
|
6
|
+
module Definable
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
class_attribute :prop_definitions, instance_accessor: false, default: HashWithIndifferentAccess.new
|
|
11
|
+
class_attribute :undefined_props_rejected, instance_accessor: false, default: nil
|
|
12
|
+
|
|
13
|
+
attr_reader :props
|
|
14
|
+
attr_reader :raw_props
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class_methods do
|
|
18
|
+
def prop(key, options = {})
|
|
19
|
+
definition = Definition.new(key, options, component: name)
|
|
20
|
+
self.prop_definitions = prop_definitions.merge(key => definition)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def reject_undefined_props!
|
|
24
|
+
self.undefined_props_rejected = true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def permit_undefined_props!
|
|
28
|
+
self.undefined_props_rejected = false
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def setup_props_for(props)
|
|
33
|
+
enforce_defined_props!(props) if undefined_props_rejected?
|
|
34
|
+
|
|
35
|
+
indifferent_props = props.with_indifferent_access
|
|
36
|
+
@raw_props = props.dup.freeze
|
|
37
|
+
@props = resolve_props(indifferent_props).freeze
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def after_initialize; end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def undefined_props_rejected?
|
|
45
|
+
per_class_setting = self.class.undefined_props_rejected
|
|
46
|
+
return per_class_setting unless per_class_setting.nil?
|
|
47
|
+
|
|
48
|
+
ViewComponentProps.configuration.reject_undefined_props
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def enforce_defined_props!(props)
|
|
52
|
+
declared_props = self.class.prop_definitions.keys.map(&:to_s)
|
|
53
|
+
unknown_props = props.keys.map(&:to_s) - declared_props
|
|
54
|
+
return if unknown_props.empty?
|
|
55
|
+
|
|
56
|
+
raise UnknownPropsError, "Unknown props for #{self.class.name}: #{unknown_props.map(&:to_sym).inspect}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve_props(props)
|
|
60
|
+
self.class.prop_definitions.each_with_object(props.dup) do |(key, definition), resolved|
|
|
61
|
+
resolved[key] = definition.call(props, self)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponentProps
|
|
4
|
+
class Definition
|
|
5
|
+
OPTION_KEYS = %i[
|
|
6
|
+
default
|
|
7
|
+
fallback
|
|
8
|
+
required
|
|
9
|
+
cast
|
|
10
|
+
enum
|
|
11
|
+
validate
|
|
12
|
+
description
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :key, :description
|
|
16
|
+
|
|
17
|
+
def initialize(key, options = {}, component:)
|
|
18
|
+
@key = key
|
|
19
|
+
@options = options
|
|
20
|
+
@component = component
|
|
21
|
+
|
|
22
|
+
validate_options!
|
|
23
|
+
|
|
24
|
+
cast = normalize_cast(@options[:cast])
|
|
25
|
+
@caster = build_caster(cast)
|
|
26
|
+
@cast_label = describe_cast(cast)
|
|
27
|
+
@required = @options[:required] || false
|
|
28
|
+
@enum = @options[:enum]
|
|
29
|
+
@validate = @options[:validate]
|
|
30
|
+
@default = @options[:default]
|
|
31
|
+
@fallback = @options[:fallback]
|
|
32
|
+
@description = @options[:description]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def call(props, instance = nil)
|
|
36
|
+
value = evaluate(props, instance)
|
|
37
|
+
casted_value = cast_value(value)
|
|
38
|
+
|
|
39
|
+
raise RequiredPropError, "Required prop :#{@key} for #{@component} cannot be nil" if casted_value.nil? && @required
|
|
40
|
+
return casted_value if casted_value.nil?
|
|
41
|
+
|
|
42
|
+
raise InvalidEnumValueError, "Prop :#{@key} for #{@component} must be one of #{@enum.inspect}, got #{casted_value.inspect}" if @enum && !@enum.include?(casted_value)
|
|
43
|
+
raise ValidationFailedError, "Prop :#{@key} for #{@component} failed validation, got #{casted_value.inspect}" if @validate && !@validate.call(casted_value)
|
|
44
|
+
|
|
45
|
+
casted_value
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def validate_options!
|
|
51
|
+
unknown_keys = @options.keys - OPTION_KEYS
|
|
52
|
+
raise UnknownOptionError, "Unknown options for prop :#{@key}: #{unknown_keys.inspect}" if unknown_keys.any?
|
|
53
|
+
|
|
54
|
+
return unless @options.key?(:cast)
|
|
55
|
+
|
|
56
|
+
cast_option = @options[:cast]
|
|
57
|
+
raise UnknownCastError, "Cast type for prop :#{@key} cannot be nil" if cast_option.nil?
|
|
58
|
+
return if cast_option.respond_to?(:call)
|
|
59
|
+
return if cast_option.is_a?(Symbol) || cast_option.is_a?(String)
|
|
60
|
+
|
|
61
|
+
raise UnknownCastError, "Cast type for prop :#{@key} must be a Symbol, String, or callable, got #{cast_option.inspect}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def normalize_cast(value)
|
|
65
|
+
return nil if value.nil?
|
|
66
|
+
return value if value.respond_to?(:call)
|
|
67
|
+
|
|
68
|
+
value.to_sym
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def evaluate(props, instance)
|
|
72
|
+
props = props.with_indifferent_access unless props.is_a?(HashWithIndifferentAccess)
|
|
73
|
+
value = begin
|
|
74
|
+
if props.key?(@key)
|
|
75
|
+
props[@key]
|
|
76
|
+
elsif @options.key?(:default)
|
|
77
|
+
resolve(@default, instance)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
value = resolve(@fallback, instance) if value.nil? && @options.key?(:fallback)
|
|
82
|
+
value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def build_caster(cast)
|
|
86
|
+
return nil if cast.nil?
|
|
87
|
+
return cast if cast.respond_to?(:call)
|
|
88
|
+
|
|
89
|
+
->(value) { Casters.fetch(cast).call(value) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def cast_value(value)
|
|
93
|
+
return value if value.nil? || @caster.nil?
|
|
94
|
+
|
|
95
|
+
@caster.call(value)
|
|
96
|
+
rescue ViewComponentProps::Error
|
|
97
|
+
raise
|
|
98
|
+
rescue ArgumentError, TypeError, NoMethodError, KeyError, RangeError => e
|
|
99
|
+
raise CastError, "Prop :#{@key} for #{@component} could not be cast to #{@cast_label} (got #{value.inspect}): #{e.message}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def describe_cast(cast)
|
|
103
|
+
return nil if cast.nil?
|
|
104
|
+
return cast.inspect unless cast.respond_to?(:call)
|
|
105
|
+
|
|
106
|
+
location = cast.respond_to?(:source_location) ? cast.source_location : nil
|
|
107
|
+
location ? "callable at #{location.join(':')}" : "callable"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def resolve(value, instance)
|
|
111
|
+
return value unless value.respond_to?(:call)
|
|
112
|
+
|
|
113
|
+
instance ? instance.instance_exec(&value) : value.call
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponentProps
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class UnknownOptionError < Error; end
|
|
7
|
+
class UnknownCastError < Error; end
|
|
8
|
+
class CastError < Error; end
|
|
9
|
+
class RequiredPropError < Error; end
|
|
10
|
+
class InvalidEnumValueError < Error; end
|
|
11
|
+
class ValidationFailedError < Error; end
|
|
12
|
+
class UnknownPropsError < Error; end
|
|
13
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ViewComponentProps
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
config.view_component_props = ActiveSupport::OrderedOptions.new
|
|
6
|
+
config.view_component_props.auto_include = true
|
|
7
|
+
|
|
8
|
+
initializer "view_component_props.install", after: :load_config_initializers do |application|
|
|
9
|
+
ViewComponentProps.install! if application.config.view_component_props.auto_include
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/class/attribute"
|
|
4
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
5
|
+
require "view_component"
|
|
6
|
+
|
|
7
|
+
require_relative "view_component_props/configuration"
|
|
8
|
+
require_relative "view_component_props/errors"
|
|
9
|
+
require_relative "view_component_props/casters"
|
|
10
|
+
require_relative "view_component_props/definition"
|
|
11
|
+
require_relative "view_component_props/definable"
|
|
12
|
+
require_relative "view_component_props/version"
|
|
13
|
+
|
|
14
|
+
module ViewComponentProps
|
|
15
|
+
class << self
|
|
16
|
+
def install!(target = ViewComponent::Base)
|
|
17
|
+
return if target.include?(Definable)
|
|
18
|
+
|
|
19
|
+
target.include(Definable)
|
|
20
|
+
target.class_eval do
|
|
21
|
+
def initialize(props = {})
|
|
22
|
+
super()
|
|
23
|
+
setup_props_for(props)
|
|
24
|
+
after_initialize
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if defined?(Rails::Railtie)
|
|
32
|
+
require_relative "view_component_props/railtie"
|
|
33
|
+
elsif ViewComponentProps.configuration.auto_include
|
|
34
|
+
ViewComponentProps.install!
|
|
35
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: view_component-props
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kinnell Shah
|
|
@@ -9,6 +9,26 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activemodel
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '6.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9.0'
|
|
12
32
|
- !ruby/object:Gem::Dependency
|
|
13
33
|
name: activesupport
|
|
14
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -49,8 +69,10 @@ dependencies:
|
|
|
49
69
|
- - "<"
|
|
50
70
|
- !ruby/object:Gem::Version
|
|
51
71
|
version: '5.0'
|
|
52
|
-
description: 'A ViewComponent extension
|
|
53
|
-
|
|
72
|
+
description: 'A ViewComponent extension that adds a `prop` DSL with defaults, fallbacks,
|
|
73
|
+
required props, casting, enum validation, custom validators, and a pluggable caster
|
|
74
|
+
registry. Patches ViewComponent::Base via a Rails Railtie (or on require outside
|
|
75
|
+
Rails) so components accept a props hash out of the box.
|
|
54
76
|
|
|
55
77
|
'
|
|
56
78
|
email:
|
|
@@ -62,10 +84,15 @@ files:
|
|
|
62
84
|
- CHANGELOG.md
|
|
63
85
|
- LICENSE
|
|
64
86
|
- README.md
|
|
65
|
-
- lib/
|
|
66
|
-
- lib/
|
|
67
|
-
- lib/
|
|
68
|
-
- lib/
|
|
87
|
+
- lib/view_component_props.rb
|
|
88
|
+
- lib/view_component_props/casters.rb
|
|
89
|
+
- lib/view_component_props/casters/base.rb
|
|
90
|
+
- lib/view_component_props/configuration.rb
|
|
91
|
+
- lib/view_component_props/definable.rb
|
|
92
|
+
- lib/view_component_props/definition.rb
|
|
93
|
+
- lib/view_component_props/errors.rb
|
|
94
|
+
- lib/view_component_props/railtie.rb
|
|
95
|
+
- lib/view_component_props/version.rb
|
|
69
96
|
homepage: https://github.com/kinnell/view_component-props
|
|
70
97
|
licenses:
|
|
71
98
|
- MIT
|
data/lib/view_component/props.rb
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "active_support/concern"
|
|
4
|
-
require "active_support/dependencies/autoload"
|
|
5
|
-
require "active_support/core_ext/hash/indifferent_access"
|
|
6
|
-
require "view_component"
|
|
7
|
-
|
|
8
|
-
require_relative "props/configuration"
|
|
9
|
-
require_relative "props/errors"
|
|
10
|
-
require_relative "props/version"
|
|
11
|
-
|
|
12
|
-
module ViewComponent
|
|
13
|
-
module Props
|
|
14
|
-
extend ActiveSupport::Concern
|
|
15
|
-
extend ActiveSupport::Autoload
|
|
16
|
-
|
|
17
|
-
class << self
|
|
18
|
-
def configuration
|
|
19
|
-
@configuration ||= Configuration.new
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def configure
|
|
23
|
-
yield(configuration)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def reset_configuration!
|
|
27
|
-
@configuration = Configuration.new
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|